Files
Genarrative/apps/ai-game-creator-shell/src/services/clientAuth.ts
T
kdletters 8701f43f75 AGC 渲染层网络与状态下沉 Rust,并修复画布交互与失败文案
- 删除渲染层传输层:services/clientApi.ts、clientHttp 的 fetchClientHttp 与超时封装整体退场,账户、认证、素材直传、错误上报与游戏分发发布改走 Rust typed command。
- 新增宿主封装:accountHost、editorAssetHost、platformSessionRetry、agentRuntimeErrorDetail 与 features/app-shell/workspacePreferences;渲染层不再读写 access token / refresh cookie,localStorage 只剩测试态旧键迁移 shim。
- 修复 DirectProject 失败文案 v2 映射与「查看详情」按需读取,画布标签面板高度夹紧、框选每帧重算、文档预览入口与场景标题栏 key、资源筛选面板样式。
- 补齐与调整回归:accountHost、agentRuntimeErrorDetail、clientAuthHost、clientHttp、platformSession、platformSessionRetry、resourceFilterPanelStyle、directThreadChatSubscription 等定向用例,并清理测试 harness 中无调用方的残留。
2026-09-28 14:15:02 +08:00

187 lines
5.5 KiB
TypeScript

import type { AuthUser } from '../../../../packages/shared/src/contracts/auth';
import { resolveTauriInvoke } from '../app/tauri';
import { subscribeTauriEvent } from './tauriEventSubscription';
/** Rust 认证态事件:只承载状态投影,不含 token 或 refresh 凭据。 */
export const CLIENT_AUTH_STATE_CHANGED_EVENT = 'agc-client-auth-state-changed';
export type ClientAuthState =
| { status: 'authenticated'; user: AuthUser; apiBaseUrl: string }
| { status: 'unauthenticated' }
| { status: 'unavailable'; message: string };
export type ClientAuthRefreshResult =
| { status: 'refreshed'; user: AuthUser }
| { status: 'unauthenticated' }
| { status: 'stale' }
| { status: 'failed'; message: string; authoritative: boolean };
export type ClientLoginCodeResult = {
cooldownSeconds: number;
expiresInSeconds: number;
};
export function normalizeAuthPhoneInput(phone: string) {
const compactPhone = phone.replace(/[^\d+]/gu, '').trim();
const mainlandChinaInternationalPhone =
compactPhone.match(/^\+?86(1\d{10})$/u);
return mainlandChinaInternationalPhone?.[1] ?? compactPhone;
}
function requireInvoke() {
const invoke = resolveTauriInvoke();
if (!invoke) {
throw new Error('需要在 Tauri App 内登录');
}
return invoke;
}
export function getClientAuthErrorMessage(error: unknown, fallback: string) {
if (error instanceof Error && error.message.trim()) return error.message;
const message = String(error ?? '').trim();
return message || fallback;
}
type RustAuthStateView = {
status?: string;
user?: AuthUser | null;
apiBaseUrl?: string | null;
errorKind?: string | null;
errorMessage?: string | null;
};
/**
* 恢复登录态。
*
* 凭据续期、当前用户复核与本机运行时会话安装都在 Rust 内完成;这里只把结果投影成
* `authenticated` / `unauthenticated` / `unavailable` 三态,供登录页决定展示分支。
*/
export async function readClientAuthState(
expectedApiBaseUrl?: string,
): Promise<ClientAuthState> {
const invoke = requireInvoke();
const view = await invoke<RustAuthStateView>('read_client_auth_state', {
expectedApiBaseUrl: expectedApiBaseUrl?.trim() || null,
});
if (view?.status === 'authenticated' && view.user && view.apiBaseUrl) {
return {
status: 'authenticated',
user: view.user,
apiBaseUrl: view.apiBaseUrl,
};
}
if (view?.status === 'unavailable') {
return {
status: 'unavailable',
message: view.errorMessage?.trim() || '登录服务暂时不可用,请稍后重试',
};
}
return { status: 'unauthenticated' };
}
export async function sendClientPhoneLoginCode(
phone: string,
apiBaseUrl: string,
): Promise<ClientLoginCodeResult> {
const invoke = requireInvoke();
const result = await invoke<{
cooldownSeconds?: number;
expiresInSeconds?: number;
}>('send_client_phone_login_code', {
apiBaseUrl,
phone: normalizeAuthPhoneInput(phone),
});
return {
cooldownSeconds: Number(result?.cooldownSeconds ?? 0),
expiresInSeconds: Number(result?.expiresInSeconds ?? 0),
};
}
export async function loginClientWithPassword(
phone: string,
password: string,
apiBaseUrl: string,
): Promise<AuthUser> {
const invoke = requireInvoke();
return invoke<AuthUser>('login_client_with_password', {
apiBaseUrl,
phone: normalizeAuthPhoneInput(phone),
password: password.trim(),
});
}
export async function loginClientWithPhoneCode(
phone: string,
code: string,
apiBaseUrl: string,
): Promise<AuthUser> {
const invoke = requireInvoke();
return invoke<AuthUser>('login_client_with_phone_code', {
apiBaseUrl,
phone: normalizeAuthPhoneInput(phone),
code: code.trim(),
});
}
/** 登出:Rust 负责服务端撤销、凭据清除与本机运行时会话清理。 */
export async function logoutClientAuthSession(): Promise<void> {
const invoke = requireInvoke();
await invoke('logout_client_session');
}
export async function refreshClientAuthSession(
expectedUserId?: string,
): Promise<ClientAuthRefreshResult> {
const invoke = requireInvoke();
const view = await invoke<{
status?: string;
user?: AuthUser | null;
authoritative?: boolean;
errorMessage?: string | null;
}>('refresh_client_auth_session', {
expectedUserId: expectedUserId?.trim() || null,
});
switch (view?.status) {
case 'refreshed':
if (!view.user) {
return {
status: 'failed',
message: '刷新登录状态失败',
authoritative: false,
};
}
return { status: 'refreshed', user: view.user };
case 'unauthenticated':
return { status: 'unauthenticated' };
case 'stale':
return { status: 'stale' };
default:
return {
status: 'failed',
message: view?.errorMessage?.trim() || '刷新登录状态失败',
authoritative: view?.authoritative === true,
};
}
}
/** 订阅 Rust 认证态事件,返回幂等释放函数。 */
export function subscribeClientAuthState(
listener: (state: ClientAuthState) => void,
): Promise<() => void> {
return subscribeTauriEvent<RustAuthStateView>(
CLIENT_AUTH_STATE_CHANGED_EVENT,
(event) => {
const view = event.payload;
if (view?.status === 'authenticated' && view.user && view.apiBaseUrl) {
listener({
status: 'authenticated',
user: view.user,
apiBaseUrl: view.apiBaseUrl,
});
return;
}
listener({ status: 'unauthenticated' });
},
).catch(() => () => {});
}