mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-19 23:42:03 +08:00
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
|
|
import type { SemanticAppState, SemanticElement } from '@/modules/computer-use/semantics/semantic-types.js';
|
|
|
|
const DEFAULT_STATE_TTL_MS = Number.parseInt(process.env.CLOUDCLI_COMPUTER_SEMANTIC_STATE_TTL_MS || String(10 * 60 * 1000), 10);
|
|
|
|
type StoredState = {
|
|
sessionId: string;
|
|
appKey: string;
|
|
state: SemanticAppState;
|
|
updatedAt: number;
|
|
};
|
|
|
|
function normalizeAppKey(app: string): string {
|
|
return app.trim().toLowerCase();
|
|
}
|
|
|
|
export class SemanticSessionStore {
|
|
private states = new Map<string, StoredState>();
|
|
private latestBySessionApp = new Map<string, string>();
|
|
|
|
createStateId(): string {
|
|
return `state_${randomUUID()}`;
|
|
}
|
|
|
|
save(sessionId: string, state: SemanticAppState): SemanticAppState {
|
|
const appKey = normalizeAppKey(state.app);
|
|
const nextState = {
|
|
...state,
|
|
stateId: state.stateId || this.createStateId(),
|
|
};
|
|
this.states.set(nextState.stateId, {
|
|
sessionId,
|
|
appKey,
|
|
state: nextState,
|
|
updatedAt: Date.now(),
|
|
});
|
|
this.latestBySessionApp.set(this.latestKey(sessionId, appKey), nextState.stateId);
|
|
return nextState;
|
|
}
|
|
|
|
getState(sessionId: string, app: string, stateId?: string): SemanticAppState | null {
|
|
this.expire();
|
|
if (stateId) {
|
|
const entry = this.states.get(stateId);
|
|
const appKey = normalizeAppKey(app);
|
|
return entry && entry.sessionId === sessionId && entry.appKey === appKey ? entry.state : null;
|
|
}
|
|
const latestStateId = this.latestBySessionApp.get(this.latestKey(sessionId, normalizeAppKey(app)));
|
|
return latestStateId ? this.states.get(latestStateId)?.state || null : null;
|
|
}
|
|
|
|
getElement(sessionId: string, app: string, elementIndex: string, stateId?: string): SemanticElement | null {
|
|
const state = this.getState(sessionId, app, stateId);
|
|
return state?.elements.find((element) => element.index === elementIndex) || null;
|
|
}
|
|
|
|
clearSession(sessionId: string): void {
|
|
for (const [stateId, entry] of this.states.entries()) {
|
|
if (entry.sessionId === sessionId) {
|
|
this.states.delete(stateId);
|
|
this.latestBySessionApp.delete(this.latestKey(entry.sessionId, entry.appKey));
|
|
}
|
|
}
|
|
}
|
|
|
|
expire(now = Date.now()): void {
|
|
const ttl = Number.isFinite(DEFAULT_STATE_TTL_MS) && DEFAULT_STATE_TTL_MS > 0
|
|
? DEFAULT_STATE_TTL_MS
|
|
: 10 * 60 * 1000;
|
|
for (const [stateId, entry] of this.states.entries()) {
|
|
if (now - entry.updatedAt > ttl) {
|
|
this.states.delete(stateId);
|
|
const key = this.latestKey(entry.sessionId, entry.appKey);
|
|
if (this.latestBySessionApp.get(key) === stateId) {
|
|
this.latestBySessionApp.delete(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private latestKey(sessionId: string, appKey: string): string {
|
|
return `${sessionId}:${appKey}`;
|
|
}
|
|
}
|
|
|
|
export const semanticSessionStore = new SemanticSessionStore();
|