wip: integrate filegraph into session

This commit is contained in:
Justin Lin committed 2025-04-24 22:05:10 +08:00
1 parent cdd22634e0
commit 2d95ca382f
13 files changed
+104 -41

No files matched your search

+12 -18
View File
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { FileGraph, FileObject } from '@linux-odyssey/file-graph' import { FileGraph, FileGraphUpdateEvent } from '@linux-odyssey/file-graph'
import { IResponse, ITask } from '@linux-odyssey/game' import { IResponse, ITask } from '@linux-odyssey/game'
import type { SessionDetail } from 'server/src/routers/sessionRouter'
import Socket from '../utils/socket' import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal' import SocketTerminal from '../utils/terminal'
import { Session } from '../types' import { Session } from '../types'
@@ -21,6 +22,7 @@ interface SessionUpdate {
status: string status: string
responses: IResponse[] responses: IResponse[]
tasks: ITask[] tasks: ITask[]
graphUpdate: FileGraphUpdateEvent
} }
interface Store { interface Store {
@@ -40,12 +42,14 @@ const useSession = defineStore('session', {
this.quest = await trpc.quests.getQuestDetail.query(questId) this.quest = await trpc.quests.getQuestDetail.query(questId)
this.questId = questId this.questId = questId
}, },
async setSession(session: Session) { async setSession(session: SessionDetail) {
console.log('set session', session) console.log('set session', session)
this.session = session this.session = {
// this.session.graph = new FileGraph(session.graph) ...session,
graph: new FileGraph(session.graph),
}
term.reset() term.reset()
await socket.connect(session) await socket.connect(this.session)
term.focus() term.focus()
}, },
async createSession() { async createSession() {
@@ -63,24 +67,14 @@ const useSession = defineStore('session', {
this.setSession(session) this.setSession(session)
} }
}, },
// updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
// if (!this.session) return
// if (event.discover) {
// this.session.graph.discover(event.discover)
// }
// if (event.pwd) {
// this.session.pwd = event.pwd
// }
// },
newUpdate(update: SessionUpdate) { newUpdate(update: SessionUpdate) {
if (!this.session) return if (!this.session) return
this.session.responses = update.responses this.session.responses = update.responses
this.session.tasks = update.tasks this.session.tasks = update.tasks
this.session.status = update.status this.session.status = update.status
// this.session.hints.push(response.hints) if (update.graphUpdate) {
// if (response.tasks) { this.session.graph.handleEvent(update.graphUpdate)
// this.session.tasks = response.tasks }
// }
// if ( // if (
// response.status === 'finished' && // response.status === 'finished' &&
// this.session.status !== 'finished' // this.session.status !== 'finished'
+1 -1
View File
@@ -17,7 +17,7 @@ export interface StageResponse {
export interface Session { export interface Session {
_id: string _id: string
status: string status: string
// graph: FileGraph graph: FileGraph
// pwd: string // pwd: string
// hints: string[][] // hints: string[][]
tasks: ITask[] tasks: ITask[]
+3 -1
View File
@@ -1,17 +1,19 @@
{ {
"name": "@linux-odyssey/file-graph", "name": "@linux-odyssey/file-graph",
"version": "1.0.0", "version": "1.0.0",
"type": "module",
"main": "./dist/cjs/index.js", "main": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
"exports": { "exports": {
".": { ".": {
"require": "./dist/cjs/index.js", "require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts" "types": "./dist/types/index.d.ts"
} }
}, },
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "tsc -p tsconfig.cjs.json", "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json",
"dev": "tsc -w", "dev": "tsc -w",
"test": "yarn build && jest" "test": "yarn build && jest"
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { FileObject } from './file.js' import { FileObject } from './file.js'
import FileNode from './fileNode.js' import FileNode from './fileNode.js'
export interface Event { export interface FileGraphUpdateEvent {
add?: FileObject[] add?: FileObject[]
remove?: FileObject[] remove?: FileObject[]
discover?: FileObject[] discover?: FileObject[]
@@ -42,7 +42,7 @@ export default class FileGraph extends FileNode {
this.merge(newNode) this.merge(newNode)
} }
handleEvent(event: Event) { handleEvent(event: FileGraphUpdateEvent) {
// eslint-disable-next-line default-case // eslint-disable-next-line default-case
switch (true) { switch (true) {
case !!event.add: case !!event.add:
+9 -2
View File
@@ -6,10 +6,17 @@ import {
} from './errors.js' } from './errors.js'
import { dirname } from './utils.js' import { dirname } from './utils.js'
export default class FileNode extends File { export interface IFileNode {
path: string
type: 'file' | 'directory'
discovered: boolean
children?: IFileNode[]
}
export default class FileNode extends File implements IFileNode {
children: FileNode[] children: FileNode[]
constructor(file: FileObject | FileNode) { constructor(file: FileObject | IFileNode) {
super(file) super(file)
this.children = [] this.children = []
if ((<FileNode>file).children) if ((<FileNode>file).children)
+2 -9
View File
@@ -1,10 +1,3 @@
export { default as File, FileObject } from './file.js' export { default as File, FileObject } from './file.js'
export { default as FileNode } from './fileNode.js' export { default as FileNode, IFileNode } from './fileNode.js'
export { default as FileGraph, Event } from './fileGraph.js' export { default as FileGraph, FileGraphUpdateEvent } from './fileGraph.js'
export interface IFileNode {
path: string
type: string
discovered: boolean
children?: IFileNode[]
}
+4 -1
View File
@@ -4,8 +4,11 @@
"rootDir": "src", "rootDir": "src",
"composite": true, "composite": true,
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist/esm", "outDir": "./dist/esm",
"declarationDir": "./dist/types" "declarationDir": "./dist/types",
"target": "ES2020",
"useDefineForClassFields": true
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
+5 -1
View File
@@ -7,6 +7,7 @@ import {
IFileExistenceChecker, IFileExistenceChecker,
IResponse, IResponse,
ITask, ITask,
ICommandParams,
} from './schema' } from './schema'
export class GameSession { export class GameSession {
@@ -22,7 +23,7 @@ export class GameSession {
this.completed = completedEvents || [] this.completed = completedEvents || []
this.quest = new Quest(quest, checker) this.quest = new Quest(quest, checker)
this.graph = new FileGraph( this.graph = new FileGraph(
graph ?? new FileNode({ path: '/', type: 'root', discovered: false }) graph ?? { path: '/', type: 'directory', discovered: false }
) )
} }
@@ -39,6 +40,9 @@ export class GameSession {
} }
async runCommand(command: ICommand) { async runCommand(command: ICommand) {
if (command.params) {
this.graph.handleEvent(command.params)
}
const stageId = await this.quest.findSatisfiedEvent(command, this.completed) const stageId = await this.quest.findSatisfiedEvent(command, this.completed)
if (stageId) { if (stageId) {
this.complete(stageId) this.complete(stageId)
+43
View File
@@ -168,4 +168,47 @@ describe('Session', () => {
{ id: 'stage4', name: 'Stage 4', completed: false }, { id: 'stage4', name: 'Stage 4', completed: false },
]) ])
}) })
it('should update graph', () => {
const session = new GameSession({}, quest, new MockFileChecker())
expect(session.getGraph()).toMatchObject({
path: '/',
type: 'directory',
discovered: false,
})
session.runCommand({
command: 'ls',
params: {
discover: [
{
path: '/home/user',
type: 'directory',
discovered: true,
},
],
},
})
expect(session.getGraph()).toMatchObject({
path: '/',
type: 'directory',
discovered: false,
children: [
{
path: '/home',
type: 'directory',
discovered: false,
children: [
{
path: '/home/user',
type: 'directory',
discovered: true,
},
],
},
],
})
})
}) })
+4 -1
View File
@@ -2,14 +2,17 @@ import { model, Schema, Types } from 'mongoose'
export interface INode { export interface INode {
path: string path: string
type: string type: 'file' | 'directory'
discovered: boolean discovered: boolean
children?: INode[] children?: INode[]
} }
const nodeSchema = new Schema<INode>({ const nodeSchema = new Schema<INode>({
path: String, path: String,
type: {
type: String, type: String,
enum: ['file', 'directory'],
},
discovered: { discovered: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -54,6 +54,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
await c.save() await c.save()
if (event) { if (event) {
session.completedEvents = gameSession.completedEvents session.completedEvents = gameSession.completedEvents
session.graph = gameSession.getGraph()
if (gameSession.isFinished()) { if (gameSession.isFinished()) {
await finishSession(session) await finishSession(session)
@@ -64,6 +65,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
responses: gameSession.getResponses(), responses: gameSession.getResponses(),
tasks: gameSession.getTasks(), tasks: gameSession.getTasks(),
status: session.status, status: session.status,
graphUpdate: command.params,
}) })
} }
+6 -3
View File
@@ -7,6 +7,8 @@ import config from '../config.js'
import { protectedProcedure, router } from '../trpc.js' import { protectedProcedure, router } from '../trpc.js'
import { questManager } from '../models/quest.js' import { questManager } from '../models/quest.js'
export type SessionDetail = Awaited<ReturnType<typeof sessionDetail>>
function sessionDetail(session: ISession) { function sessionDetail(session: ISession) {
const quest = questManager.get(session.quest) const quest = questManager.get(session.quest)
if (!quest) { if (!quest) {
@@ -22,14 +24,15 @@ function sessionDetail(session: ISession) {
) )
return { return {
_id: session._id.toString(), _id: session._id.toString(),
user: session.user, user: session.user.toString(),
quest: session.quest, quest: session.quest,
status: session.status, status: session.status,
createdAt: session.createdAt, createdAt: session.createdAt.toISOString(),
lastActivityAt: session.lastActivityAt, lastActivityAt: session.lastActivityAt.toISOString(),
completedEvents: session.completedEvents, completedEvents: session.completedEvents,
responses: gameSession.getResponses(), responses: gameSession.getResponses(),
tasks: gameSession.getTasks(), tasks: gameSession.getTasks(),
graph: gameSession.getGraph(),
} }
} }
+11 -2
View File
@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", // 要編譯成的 JS 目標版本 "target": "ESNext", // 要編譯成的 JS 目標版本
"module": "NodeNext", // 要編譯成的 module "module": "ESNext", // 要編譯成的 module
"moduleResolution": "NodeNext", // module 解析方式, default "classic" "moduleResolution": "NodeNext", // module 解析方式, default "classic"
"esModuleInterop": true, // module 兼容, 會影響 build 方式 "esModuleInterop": true, // module 兼容, 會影響 build 方式
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
@@ -9,6 +9,15 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"declaration": true, "declaration": true,
"declarationMap": true "declarationMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["*"],
"@app/*": ["app/*"],
"@server/*": ["server/*"],
"@models/*": ["packages/models/src/*"],
"@game/*": ["packages/game/src/*"],
"@file-graph/*": ["packages/file-graph/src/*"]
}
} }
} }