mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Feat global exception (#83)
- add global exceptions - refactor quest models to reusable parts other improvements: - improve error handling
This commit is contained in:
25 files changed
+231
-201
No files matched your search
@@ -0,0 +1,19 @@
|
||||
import { Schema } from 'mongoose'
|
||||
import conditionSchema from './condition.js'
|
||||
import responseSchema from './response.js'
|
||||
import hintSchema from './hint.js'
|
||||
|
||||
const commandResponderSchema = new Schema({
|
||||
condition: {
|
||||
type: conditionSchema,
|
||||
required: false,
|
||||
},
|
||||
responses: {
|
||||
type: [responseSchema],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
commandResponderSchema.add(hintSchema)
|
||||
|
||||
export default commandResponderSchema
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Schema } from 'mongoose'
|
||||
|
||||
const fileConditionSchema = new Schema({
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: String,
|
||||
exists: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const conditionSchema = new Schema({
|
||||
command: [String],
|
||||
output: [String],
|
||||
error: [String],
|
||||
pwd: [String],
|
||||
files: [fileConditionSchema],
|
||||
})
|
||||
|
||||
conditionSchema.add({
|
||||
or: [conditionSchema],
|
||||
not: conditionSchema,
|
||||
})
|
||||
|
||||
export default conditionSchema
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Schema } from 'mongoose'
|
||||
import commandResponderSchema from './commandResponder.js'
|
||||
|
||||
const exceptionSchema = new Schema({})
|
||||
|
||||
exceptionSchema.add(commandResponderSchema)
|
||||
|
||||
export default exceptionSchema
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Schema } from 'mongoose'
|
||||
import exceptionSchema from './exception.js'
|
||||
import requirementSchema from './requirement.js'
|
||||
|
||||
const globalExceptionSchema = new Schema({})
|
||||
|
||||
globalExceptionSchema.add(exceptionSchema)
|
||||
globalExceptionSchema.add(requirementSchema)
|
||||
|
||||
export default globalExceptionSchema
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Schema } from 'mongoose'
|
||||
|
||||
const hintSchema = new Schema({
|
||||
hints: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default hintSchema
|
||||
@@ -1,98 +1,6 @@
|
||||
import { model, Schema } from 'mongoose'
|
||||
|
||||
const responseSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['narrative', 'dialogue'],
|
||||
default: 'dialogue',
|
||||
required: true,
|
||||
},
|
||||
content: [String],
|
||||
speaker: { type: String, default: 'Ada' },
|
||||
color: String,
|
||||
})
|
||||
|
||||
const fileConditionSchema = new Schema({
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: String,
|
||||
exists: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const conditionSchema = new Schema({
|
||||
command: [String],
|
||||
output: [String],
|
||||
error: [String],
|
||||
pwd: [String],
|
||||
files: [fileConditionSchema],
|
||||
})
|
||||
|
||||
conditionSchema.add({
|
||||
or: [conditionSchema],
|
||||
not: conditionSchema,
|
||||
})
|
||||
|
||||
const exceptionSchema = new Schema({
|
||||
condition: {
|
||||
type: conditionSchema,
|
||||
required: false,
|
||||
},
|
||||
catchAll: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
responses: {
|
||||
type: [responseSchema],
|
||||
required: true,
|
||||
},
|
||||
hints: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const stageSchema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
task: {
|
||||
type: String,
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
repeatable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
condition: {
|
||||
type: conditionSchema,
|
||||
required: true,
|
||||
},
|
||||
responses: {
|
||||
type: [responseSchema],
|
||||
required: true,
|
||||
},
|
||||
hints: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
exceptions: {
|
||||
type: [exceptionSchema],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
import stageSchema from './stage.js'
|
||||
import globalExceptionSchema from './globalException.js'
|
||||
|
||||
const Quest = model(
|
||||
'Quest',
|
||||
@@ -126,6 +34,10 @@ const Quest = model(
|
||||
type: [stageSchema],
|
||||
required: true,
|
||||
},
|
||||
exceptions: {
|
||||
type: [globalExceptionSchema],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Schema } from 'mongoose'
|
||||
|
||||
const requirementSchema = new Schema({
|
||||
requirements: [
|
||||
{
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default requirementSchema
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Schema } from 'mongoose'
|
||||
|
||||
const responseSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['narrative', 'dialogue'],
|
||||
default: 'dialogue',
|
||||
required: true,
|
||||
},
|
||||
content: [String],
|
||||
speaker: { type: String, default: 'Ada' },
|
||||
color: String,
|
||||
})
|
||||
|
||||
export default responseSchema
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Schema } from 'mongoose'
|
||||
import commandResponderSchema from './commandResponder.js'
|
||||
import stageExceptionSchema from './stageException.js'
|
||||
import requirementSchema from './requirement.js'
|
||||
|
||||
const stageSchema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
task: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
exceptions: {
|
||||
type: [stageExceptionSchema],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
stageSchema.add(commandResponderSchema)
|
||||
stageSchema.add(requirementSchema)
|
||||
|
||||
export default stageSchema
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Schema } from 'mongoose'
|
||||
import exceptionSchema from './exception.js'
|
||||
|
||||
const stageExceptionSchema = new Schema({
|
||||
catchAll: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
stageExceptionSchema.add(exceptionSchema)
|
||||
|
||||
export default stageExceptionSchema
|
||||
@@ -9,14 +9,6 @@ requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
@@ -73,3 +65,11 @@ stages:
|
||||
- 你成功的在終端機上顯示了 `How are you?`。
|
||||
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
||||
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
||||
|
||||
exceptions:
|
||||
- repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
@@ -9,14 +9,6 @@ requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
|
||||
@@ -10,14 +10,6 @@ requirements:
|
||||
- read
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
@@ -68,6 +60,7 @@ stages:
|
||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||
|
||||
- id: cat
|
||||
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- '`'
|
||||
hints:
|
||||
- '!! 不需要輸入上下引號 !!'
|
||||
@@ -10,14 +10,6 @@ requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
@@ -115,7 +107,7 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander/folder1$
|
||||
command:
|
||||
- ls
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||
|
||||
@@ -9,22 +9,6 @@ instruction: |
|
||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: warning-02
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- '`'
|
||||
hints:
|
||||
- '!! 不需要輸入上下引號 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
|
||||
@@ -9,14 +9,6 @@ requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
|
||||
@@ -11,14 +11,6 @@ requirements:
|
||||
- explore
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
|
||||
@@ -9,14 +9,6 @@ requirements:
|
||||
- path
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
|
||||
@@ -13,14 +13,6 @@ requirements:
|
||||
- move
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
this.session.graph = graph
|
||||
} catch (e) {
|
||||
logger.error('Update file graph failed', {
|
||||
error: e,
|
||||
error: e.message,
|
||||
session: this.session.id,
|
||||
graph: this.session.graph,
|
||||
discover: this.params.discover,
|
||||
@@ -122,9 +122,9 @@ export default class CommandHandler extends SessionHandler {
|
||||
async run() {
|
||||
try {
|
||||
this.quest = await Quest.findById(this.session.quest)
|
||||
const stages = this.getStages()
|
||||
const stages = this.getUnlockedComponents(this.quest.stages)
|
||||
if (stages.length === 0) {
|
||||
logger.warn('stage not found', this.session.tasks)
|
||||
logger.warn('stage not found', { session: this.session.id })
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -136,16 +136,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
)
|
||||
|
||||
const stage = stages.find((_, i) => matches[i])
|
||||
if (!stage) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
if (stage) {
|
||||
const response = this.execute(stage)
|
||||
|
||||
return {
|
||||
@@ -153,10 +144,36 @@ export default class CommandHandler extends SessionHandler {
|
||||
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) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await this.isMatch(exception.condition)) {
|
||||
return this.executeException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
} catch (err) {
|
||||
logger.error('CommandHandler error', {
|
||||
error: err,
|
||||
session: this.session.id,
|
||||
error: err.message,
|
||||
session: this.session._id,
|
||||
quest: this.quest._id,
|
||||
command: this.commandInput,
|
||||
})
|
||||
return {}
|
||||
|
||||
@@ -19,16 +19,14 @@ export default class SessionHandler {
|
||||
return task ? task.completed : false
|
||||
}
|
||||
|
||||
getStages() {
|
||||
return this.quest.stages.filter(
|
||||
(stage) =>
|
||||
stage.requirements.every((r) => this.isCompleted({ id: r })) &&
|
||||
(stage.repeatable || !this.isCompleted(stage))
|
||||
getUnlockedComponents(components) {
|
||||
return components.filter((component) =>
|
||||
component.requirements.every((r) => this.isCompleted({ id: r }))
|
||||
)
|
||||
}
|
||||
|
||||
getNewTasks() {
|
||||
const stages = this.getStages()
|
||||
const stages = this.getUnlockedComponents(this.quest.stages)
|
||||
return stages
|
||||
.filter((s) => s.task)
|
||||
.filter((s) => !this.session.tasks.some((t) => t.id === s.id))
|
||||
|
||||
@@ -38,7 +38,12 @@ async function main() {
|
||||
await setupTest()
|
||||
}
|
||||
|
||||
try {
|
||||
await loadAndUpdateQuests()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load quests', err)
|
||||
return
|
||||
}
|
||||
expiryRemovalScheduler()
|
||||
|
||||
const file = await fs.readFile('./swagger.yaml', 'utf8')
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import winston from 'winston'
|
||||
import winston, { format } from 'winston'
|
||||
import config from '../config.js'
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.json(),
|
||||
format: format.combine(
|
||||
format.errors({ stack: true }),
|
||||
format.timestamp(),
|
||||
format.json()
|
||||
),
|
||||
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
||||
transports: [
|
||||
//
|
||||
|
||||
@@ -7,6 +7,15 @@ import logger from './logger.js'
|
||||
|
||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
||||
|
||||
class QuestValidationError extends Error {
|
||||
constructor(message, questId, error) {
|
||||
super(message)
|
||||
this.name = 'QuestValidationError'
|
||||
this.questId = questId
|
||||
this.error = error
|
||||
}
|
||||
}
|
||||
|
||||
async function mapQuests(callback) {
|
||||
const questNames = await fs.readdir(questDirectory, {
|
||||
withFileTypes: true,
|
||||
@@ -23,12 +32,20 @@ async function mapQuests(callback) {
|
||||
)
|
||||
}
|
||||
|
||||
async function loadGlobalExceptions() {
|
||||
const exceptionsPath = path.join(questDirectory, 'exceptions.yml')
|
||||
const body = await fs.readFile(exceptionsPath, 'utf-8')
|
||||
return yaml.parse(body)
|
||||
}
|
||||
|
||||
export async function loadAndUpdateQuests() {
|
||||
await Quest.deleteMany({})
|
||||
const { exceptions } = await loadGlobalExceptions()
|
||||
console.log(exceptions)
|
||||
return mapQuests(async (id, questPath) => {
|
||||
const files = await fs.readdir(questPath)
|
||||
if (!files.includes('game.yml')) {
|
||||
throw new Error(`Quest ${id} is missing game.yml`)
|
||||
throw new QuestValidationError(`Missing game.yml`, id)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -44,10 +61,12 @@ export async function loadAndUpdateQuests() {
|
||||
image,
|
||||
...questData,
|
||||
})
|
||||
quest.exceptions.push(...exceptions)
|
||||
await quest.save()
|
||||
} catch (error) {
|
||||
logger.error(`Error parsing quest ${id}:`, error)
|
||||
throw error
|
||||
console.error(error)
|
||||
logger.error(`Error parsing quest`, { id, error })
|
||||
throw new QuestValidationError(`Error parsing quest`, id, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user