8701f43f75
- 删除渲染层传输层: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 中无调用方的残留。
187 lines
5.5 KiB
TypeScript
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(() => () => {});
|
|
}
|