refactor: streamline query requests and session storage
Generic Container CI/CD / test-build-publish (push) Successful in 2m4s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 2m4s

This commit is contained in:
2026-11-09 18:49:32 +08:00
parent 6279a4b555
commit 5ee93329a4
25 changed files with 111 additions and 100 deletions
+8 -24
View File
@@ -1,10 +1,5 @@
import { join } from "node:path";
import { config } from "../config.js";
import {
atomicWriteJson,
ensureDirectory,
listJsonFiles,
readJsonFile,
toStableId,
} from "../utils/fileStore.js";
@@ -50,11 +45,7 @@ const DEFAULT_SEARCH_MAX_QUERY_CHARS = 240;
export class SessionTranscriptStore {
private readonly writeQueues = new Map<string, Promise<void>>();
constructor(private readonly baseDir = config.SESSION_TRANSCRIPT_STORAGE_DIR, private readonly workspaces?: SessionWorkspaceStore) {}
async initialize() {
if (!this.workspaces) await ensureDirectory(this.baseDir);
}
constructor(private readonly workspaces: SessionWorkspaceStore) {}
async appendTurn(
context: SessionTranscriptContext,
@@ -66,7 +57,7 @@ export class SessionTranscriptStore {
) {
const key = this.filePath(context);
return this.serializeWrite(key, async () => {
if (this.workspaces) await this.workspaces.ensure(this.workspaceContext(context));
await this.workspaces.ensure(this.workspaceContext(context));
// 同一会话的多次写入串行化,防止流式结束和 UI 同步同时写同一个 transcript。
const transcript = (await this.readTranscript(context)) ?? {
actorKey: context.actorKey,
@@ -148,10 +139,8 @@ export class SessionTranscriptStore {
turns: projectTurnsForFork(sourceTranscript?.turns ?? [], keepMessageCount),
updatedAt: timestamp,
};
if (this.workspaces) {
await this.workspaces.ensure(this.workspaceContext(targetContext));
await this.workspaces.validate(this.workspaceContext(targetContext), "transcript.json");
}
await this.workspaces.ensure(this.workspaceContext(targetContext));
await this.workspaces.validate(this.workspaceContext(targetContext), "transcript.json");
await this.persist(targetContext, nextTranscript);
return nextTranscript;
}
@@ -168,7 +157,7 @@ export class SessionTranscriptStore {
}
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
const hits: SessionSearchHit[] = [];
const files = this.workspaces ? await this.workspaces.list(context, "transcript.json") : await listJsonFiles(this.baseDir);
const files = await this.workspaces.list(context, "transcript.json");
for (const file of files) {
const transcript = await readJsonFile<SessionTranscriptRecord>(file);
if (!transcript) {
@@ -205,7 +194,7 @@ export class SessionTranscriptStore {
}
private async readTranscript(context: SessionTranscriptContext) {
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "transcript.json")) return null;
if (!await this.workspaces.validate(this.workspaceContext(context), "transcript.json")) return null;
const record = await readJsonFile<SessionTranscriptRecord>(this.filePath(context));
if (record && (record.actorKey !== context.actorKey || record.projectKey !== context.projectKey ||
(record.clientSessionId || record.sessionId) !== (context.clientSessionId || context.sessionId))) throw new Error("chat transcript ownership mismatch");
@@ -213,16 +202,11 @@ export class SessionTranscriptStore {
}
private filePath(context: SessionTranscriptContext) {
if (this.workspaces) return this.workspaces.path(this.workspaceContext(context), "transcript.json");
return join(
this.baseDir,
`${context.actorKey}__${context.projectKey}__${context.sessionId}.json`,
);
return this.workspaces.path(this.workspaceContext(context), "transcript.json");
}
private async persist(context: SessionTranscriptContext, transcript: SessionTranscriptRecord) {
if (this.workspaces) await this.workspaces.writeRecord(this.workspaceContext(context), "transcript.json", transcript);
else await atomicWriteJson(this.filePath(context), transcript);
await this.workspaces.writeRecord(this.workspaceContext(context), "transcript.json", transcript);
}
private workspaceContext(context: SessionTranscriptContext) {
+6 -24
View File
@@ -1,14 +1,9 @@
import type { SessionWorkspaceContext } from "../runtime/conversationWorkspace.js";
import { join } from "node:path";
import { config } from "../config.js";
import { type SessionWorkspaceStore } from "./workspace-store.js";
import {
atomicWriteJson,
ensureDirectory,
readJsonFile,
removeFileIfExists,
slugify,
} from "../utils/fileStore.js";
export type SessionUiStateRecord = {
@@ -21,49 +16,36 @@ export type SessionUiStateRecord = {
type SessionUiStateContext = SessionWorkspaceContext;
export class SessionUiStateStore {
constructor(private readonly baseDir = config.SESSION_UI_STATE_STORAGE_DIR, private readonly workspaces?: SessionWorkspaceStore) {}
async initialize() {
if (!this.workspaces) await ensureDirectory(this.baseDir);
}
constructor(private readonly workspaces: SessionWorkspaceStore) {}
async read(context: SessionUiStateContext) {
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return null;
if (!await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return null;
return await readJsonFile<SessionUiStateRecord>(this.filePath(context));
}
async write(context: SessionUiStateContext, state: SessionUiStateRecord) {
if (state.sessionId !== context.sessionId) throw new Error("chat state session mismatch");
if (this.workspaces) {
await this.workspaces.ensure(this.workspaceContext(context));
await this.workspaces.validate(this.workspaceContext(context), "messages.json");
}
if (this.workspaces) await this.workspaces.writeRecord(this.workspaceContext(context), "messages.json", state);
else await atomicWriteJson(this.filePath(context), state);
await this.workspaces.writeRecord(this.workspaceContext(context), "messages.json", state);
return state;
}
async update(context: SessionUiStateContext, change: (state: SessionUiStateRecord) => SessionUiStateRecord) {
if (this.workspaces) return this.workspaces.updateRecord(this.workspaceContext(context), "messages.json", async ({ current }) => {
return this.workspaces.updateRecord(this.workspaceContext(context), "messages.json", async ({ current }) => {
const state = current as SessionUiStateRecord | null;
if (!state || state.sessionId !== context.sessionId || !Array.isArray(state.messages)) throw new Error("chat state unavailable");
const next = change(state);
if (next.sessionId !== context.sessionId) throw new Error("chat state session mismatch");
return { result: next, value: next };
});
const state = await this.read(context);
if (!state) throw new Error("chat state unavailable");
return this.write(context, change(state));
}
async remove(context: SessionUiStateContext) {
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return;
if (!await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return;
await removeFileIfExists(this.filePath(context));
}
private filePath(context: SessionUiStateContext) {
if (this.workspaces) return this.workspaces.path(this.workspaceContext(context), "messages.json");
return join(this.baseDir, `${slugify(context.sessionId)}.json`);
return this.workspaces.path(this.workspaceContext(context), "messages.json");
}
private workspaceContext(context: SessionUiStateContext) {