实现客户端登录七天滑动续期

需求:解决短效访问令牌到期后客户端一小时掉登录的问题。

实现:由 Electron Main 加密管理并轮换刷新凭据,按真实用户活动续期,七天闲置后清理会话,并补齐并发、迁移和终态回归测试。
This commit is contained in:
2026-08-07 16:11:10 +08:00
parent 7b23cce67a
commit 86ece3a430
20 changed files with 2941 additions and 326 deletions

View File

@@ -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>;
}

View File

@@ -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