实现客户端登录七天滑动续期
需求:解决短效访问令牌到期后客户端一小时掉登录的问题。 实现:由 Electron Main 加密管理并轮换刷新凭据,按真实用户活动续期,七天闲置后清理会话,并补齐并发、迁移和终态回归测试。
This commit is contained in:
@@ -22,6 +22,11 @@ export type DesignWorkspaceEventSubscriptionInput = {
|
||||
afterEventId?: string;
|
||||
};
|
||||
|
||||
export type CloseEventSessionsOptions = {
|
||||
accessToken?: string;
|
||||
tolerateRemoteFailure?: boolean;
|
||||
};
|
||||
|
||||
export class DesignWorkspaceModuleError extends Error {
|
||||
readonly status: number;
|
||||
readonly code: string;
|
||||
@@ -47,7 +52,7 @@ export interface DesignWorkspaceModule {
|
||||
openWorkspaceEvents?(
|
||||
input: DesignWorkspaceEventSubscriptionInput,
|
||||
): Promise<DesignWorkspaceEventSubscription>;
|
||||
closeEventSessions?(): Promise<void>;
|
||||
closeEventSessions?(options?: CloseEventSessionsOptions): Promise<void>;
|
||||
openAssetContent(workspaceId: string, assetId: string, range?: string): Promise<Response>;
|
||||
reset?(): Promise<DesignWorkspaceBootstrap>;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import { getValidWorksSquareAccessToken } from '../services/works-square-session';
|
||||
import {
|
||||
DesignWorkspaceModuleError,
|
||||
type CloseEventSessionsOptions,
|
||||
type DesignWorkspaceEventSubscription,
|
||||
type DesignWorkspaceEventSubscriptionInput,
|
||||
type DesignWorkspaceModule,
|
||||
@@ -1114,7 +1115,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
};
|
||||
}
|
||||
|
||||
async closeEventSessions(): Promise<void> {
|
||||
async closeEventSessions(options: CloseEventSessionsOptions = {}): Promise<void> {
|
||||
this.eventSessionsEnabled = false;
|
||||
const activeSubscriptions = [...this.eventSubscriptionClosers.values()]
|
||||
.flatMap((closers) => [...closers]);
|
||||
@@ -1122,27 +1123,42 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
for (const close of activeSubscriptions) close();
|
||||
const pendingSessions = [...this.eventSessions.entries()];
|
||||
this.eventSessions.clear();
|
||||
const sessions = await Promise.allSettled(
|
||||
pendingSessions.map(async ([workspaceId, pending]) => ({
|
||||
workspaceId,
|
||||
session: await pending,
|
||||
})),
|
||||
);
|
||||
const closeResults = await Promise.allSettled(
|
||||
sessions.flatMap((result) => (
|
||||
result.status === 'fulfilled'
|
||||
? [this.closeEventSession(result.value.session.session_id)
|
||||
.then(() => this.rotateEventSession(result.value.workspaceId))]
|
||||
: []
|
||||
)),
|
||||
);
|
||||
const uncertainCreations = sessions.filter((result) => (
|
||||
result.status === 'rejected'
|
||||
&& !(result.reason instanceof DesignWorkspaceModuleError
|
||||
&& result.reason.code === 'DESIGN_EVENT_SESSION_CLOSED')
|
||||
const sessions = await Promise.all(pendingSessions.map(async ([workspaceId, pending]) => {
|
||||
try {
|
||||
return { workspaceId, session: await pending, creationError: null as unknown };
|
||||
} catch (error) {
|
||||
return { workspaceId, session: null, creationError: error };
|
||||
}
|
||||
}));
|
||||
const closeResults = await Promise.all(sessions.map(async ({
|
||||
workspaceId,
|
||||
session,
|
||||
creationError,
|
||||
}) => {
|
||||
let remoteError = creationError;
|
||||
if (session) {
|
||||
try {
|
||||
await this.closeEventSession(session.session_id, options.accessToken);
|
||||
} catch (error) {
|
||||
remoteError = error;
|
||||
}
|
||||
}
|
||||
|
||||
let rotationError: unknown = null;
|
||||
try {
|
||||
await this.rotateEventSession(workspaceId);
|
||||
} catch (error) {
|
||||
rotationError = error;
|
||||
}
|
||||
return { remoteError, rotationError };
|
||||
}));
|
||||
const failed = closeResults.filter(({ remoteError, rotationError }) => (
|
||||
Boolean(rotationError)
|
||||
|| (!options.tolerateRemoteFailure
|
||||
&& Boolean(remoteError)
|
||||
&& !(remoteError instanceof DesignWorkspaceModuleError
|
||||
&& remoteError.code === 'DESIGN_EVENT_SESSION_CLOSED'))
|
||||
)).length;
|
||||
const failedCloses = closeResults.filter((result) => result.status === 'rejected').length;
|
||||
const failed = uncertainCreations + failedCloses;
|
||||
if (failed > 0) {
|
||||
throw new Error(`Failed to close ${failed} AI design Agent Session(s)`);
|
||||
}
|
||||
@@ -1155,7 +1171,11 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
);
|
||||
}
|
||||
|
||||
private async requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
private async requestJson<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
accessToken?: string,
|
||||
): Promise<T> {
|
||||
const response = await this.authorizedFetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
@@ -1165,7 +1185,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
: {}),
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
}, accessToken);
|
||||
const payload = await readPayload(response);
|
||||
if (!response.ok) {
|
||||
const detail = asErrorDetail(payload);
|
||||
@@ -1184,7 +1204,12 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
private async authorizedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
private async authorizedFetch(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
accessToken?: string,
|
||||
): Promise<Response> {
|
||||
if (accessToken) return this.fetchWithToken(path, accessToken, init);
|
||||
let token = await getValidWorksSquareAccessToken({ fetchImpl: this.fetchImpl });
|
||||
if (!token) {
|
||||
throw new DesignWorkspaceModuleError(401, 'AUTH_REQUIRED', '请先登录后再使用 AI 设计');
|
||||
@@ -1314,11 +1339,12 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
for (const resolve of waiters) resolve(run);
|
||||
}
|
||||
|
||||
private async closeEventSession(sessionId: string): Promise<void> {
|
||||
private async closeEventSession(sessionId: string, accessToken?: string): Promise<void> {
|
||||
try {
|
||||
await this.requestJson<ServerAgentSession>(
|
||||
`/api/agents/sessions/${encodeURIComponent(sessionId)}`,
|
||||
{ method: 'DELETE' },
|
||||
accessToken,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DesignWorkspaceModuleError
|
||||
|
||||
Reference in New Issue
Block a user