fix: save new created session

This commit is contained in:
Justin Lin committed 2025-04-22 19:06:57 +08:00
1 parent a711e08328
commit 66434ff897
5 files changed
+4 -299

No files matched your search

@@ -22,7 +22,7 @@ function sessionSummary(session: ISession) {
function sessionDetail(session: ISession) {
return {
...sessionSummary(session),
stages: session.events,
events: session.completedEvents,
}
}
-202
View File
@@ -1,202 +0,0 @@
import minimist from 'minimist'
import { HydratedDocument } from 'mongoose'
import { FileGraph } from '@linux-odyssey/file-graph'
import { ICondition, Quest, Stage, IException } from '@linux-odyssey/game'
import { ICommand, ISession } from '@linux-odyssey/models'
import { pushToSession } from '../api/socket.js'
import SessionHandler, { ExecuteResult } from './sessionHandler.js'
import { checkFile } from '../containers/cli.js'
import logger from '../utils/logger.js'
// 檢查 pattern 是否符合 input
const checkMatch = (pattern?: string[], input?: string) => {
if (!pattern || pattern.length === 0) return true
return pattern.some((p) => {
const regex = new RegExp(p)
return input && regex.test(input)
})
}
export default class CommandHandler extends SessionHandler {
commandInput: ICommand
argv: minimist.ParsedArgs
params: Record<string, any>
constructor(
session: HydratedDocument<ISession>,
quest: Quest,
commandInput: ICommand,
params: Record<string, any>
) {
super(session, quest)
this.commandInput = commandInput
this.argv = minimist(this.commandInput.command.split(' '))
this.params = params
}
handleEvent() {
if (this.params.discover) this.discoverHandler()
}
handleCommand() {
const command = this.argv._[0]
switch (command) {
case 'cd':
pushToSession(this.session.id, 'graph', {
pwd: this.commandInput.pwd,
})
break
case 'ls':
pushToSession(this.session.id, 'graph', {
pwd: this.commandInput.pwd,
discover: this.params.discover,
})
break
default:
break
}
}
async discoverHandler() {
try {
const graph = new FileGraph(this.session.graph)
graph.discover(this.params.discover)
this.session.graph = graph
} catch (e) {
logger.error('Update file graph failed', {
error: (e as Error).message,
session: this.session.id,
graph: this.session.graph,
discover: this.params.discover,
})
}
}
async isMatch(condition: Condition): Promise<boolean> {
return (
this.checkKeys(condition) &&
(await this.checkFiles(condition.files)) &&
(await this.checkNot(condition.not)) &&
(await this.checkOr(condition.or)) === true
)
}
checkKeys(condition: Condition) {
return (
checkMatch(condition.command, this.commandInput.command) &&
checkMatch(condition.output, this.commandInput.output) &&
checkMatch(condition.error, this.commandInput.error) &&
checkMatch(condition.pwd, this.commandInput.pwd)
)
}
async checkFiles(files?: FileCondition[]): Promise<boolean> {
if (!files || files.length === 0) {
return true
}
try {
const checks = await Promise.all(
files.map(
(f) =>
this.session.containerId && checkFile(this.session.containerId, f)
)
)
return checks.every((c) => c === true)
} catch {
return false
}
}
async checkNot(condition?: Condition): Promise<boolean> {
if (!condition) {
return true
}
return !(await this.isMatch(condition))
}
async checkOr(conditions?: Condition[]): Promise<boolean> {
if (!conditions || conditions.length === 0) {
return true
}
const matches = await Promise.all(conditions.map((c) => this.isMatch(c)))
return matches.some((m) => m === true)
}
async checkException(stage: Stage): Promise<Exception | null> {
const exceptions = stage.exceptions || []
for (const exception of exceptions) {
// eslint-disable-next-line no-await-in-loop
if (exception.condition && (await this.isMatch(exception.condition))) {
return exception
}
if (exception.catchAll) return exception
}
return null
}
async run(): Promise<ExecuteResult | null> {
try {
const stages = this.getStages()
if (stages.length === 0) {
logger.warn('stage not found', { session: this.session.id })
return null
}
this.handleEvent()
this.handleCommand()
const matches = await Promise.all(
stages.map((stage) => stage.condition && this.isMatch(stage.condition))
)
const stage = stages.find((_, i) => matches[i])
if (stage) {
const response = this.execute(stage)
return {
stage: stage.id,
end: this.session.status === 'finished',
...response,
}
}
// stage exception
for (const s of stages) {
// eslint-disable-next-line no-await-in-loop
const exception = await this.checkException(s)
if (exception) {
return this.executeException(exception)
}
}
// global exception
if (this.quest.exceptions.length > 0) {
const globalExceptions = this.getUnlockedComponents(
this.quest.exceptions
)
for (const exception of globalExceptions) {
if (
exception.condition &&
// eslint-disable-next-line no-await-in-loop
(await this.isMatch(exception.condition))
) {
return this.executeException(exception)
}
}
}
return null
} catch (err) {
logger.error('CommandHandler error', {
error: (err as Error).message,
session: this.session._id,
quest: this.quest.id,
command: this.commandInput,
})
return null
}
}
}
-94
View File
@@ -1,94 +0,0 @@
import { HydratedDocument } from 'mongoose'
import type {
Quest,
Response,
Requirement,
Stage,
Task,
Exception,
} from '@linux-odyssey/constants'
import { ISession } from '@linux-odyssey/models'
export interface ExecuteResult {
responses: Response[]
status: string
stage?: string
end?: boolean
}
export default class SessionHandler {
session: HydratedDocument<ISession>
quest: Quest
constructor(session: HydratedDocument<ISession>, quest: Quest) {
this.session = session
this.quest = quest
}
getSession(): HydratedDocument<ISession> {
return this.session
}
getTask(stage: Partial<Stage>): Task | undefined {
return this.session.tasks.find((t) => t.id === stage.id)
}
isCompleted(stage: Partial<IStage>): boolean {
const task = this.getTask(stage)
return task ? task.completed : false
}
getUnlockedComponents<T extends { requirements: Requirement }>(
components: T[]
): T[] {
return components.filter((component) =>
component.requirements.every((r: string) => this.isCompleted({ id: r }))
)
}
getStages() {
const unlockedStages = this.getUnlockedComponents(this.quest.stages)
return unlockedStages.filter((s) => !this.isCompleted(s))
}
getNewTasks() {
const stages = this.getStages()
return stages
.filter((s) => s.task)
.filter((s) => !this.session.tasks.some((t) => t.id === s.id))
.map((s) => ({ id: s.id, name: s.task, completed: false }))
}
addNewTasks() {
const newTasks = this.getNewTasks()
this.session.tasks.push(...newTasks)
}
execute(stage: Stage): ExecuteResult {
this.session.tasks
.filter((t) => t.id === stage.id)
.forEach((t) => {
t.completed = true
})
this.addNewTasks()
if (stage.id === 'END') {
this.session.status = 'finished'
}
return {
responses: stage.responses,
status: this.session.status,
}
}
executeException(exception: Exception): ExecuteResult {
const { responses } = exception
return {
responses,
status: this.session.status,
}
}
}
+2 -1
View File
@@ -49,7 +49,7 @@ export async function createNewSession(
const newSession = new Session({
user,
quest,
quest: quest.id,
containerId: container.id,
})
@@ -70,6 +70,7 @@ export async function createNewSession(
progress.sessions.push(newSession._id)
}
await userProfile.save()
await newSession.save()
return newSession
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const sessionRouter = router({
status: session.status,
createdAt: session.createdAt,
lastActivityAt: session.lastActivityAt,
stages: session.events,
completedEvents: session.completedEvents,
}
}
throw new TRPCError({