mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 11:30:13 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc1d3f6ce5 | ||
|
|
5cfe6823cc | ||
|
|
6cd348433a | ||
|
|
617bb56f5f | ||
|
|
a859b4ff3e | ||
|
|
46fb8407a6 | ||
|
|
c7d18ad95e | ||
|
|
8f31e4f2cd | ||
|
|
d4446186cd | ||
|
|
8fb8af036d | ||
|
|
68492cd932 | ||
|
|
3c1afb92e1 | ||
|
|
abaacf7ee7 | ||
|
|
a22b898c0c | ||
|
|
d64a4ae81d | ||
|
|
ebd81f7cb4 | ||
|
|
f657f633b4 | ||
|
|
fb6411e923 | ||
|
|
a7e646c28b | ||
|
|
98a226f3ff | ||
|
|
49fb5a6a61 | ||
|
|
fe69841622 | ||
|
|
bb968706ad | ||
|
|
849e669147 | ||
|
|
b41e67ee0b | ||
|
|
50a0bbbab1 | ||
|
|
d712925d0f | ||
|
|
b78398680d | ||
|
|
c7f52b1003 | ||
|
|
f008fb9f60 | ||
|
|
76a39add29 | ||
|
|
eb9118d5b1 | ||
|
|
cfdf8730e8 | ||
|
|
6651071580 |
No files matched your search
@@ -20,7 +20,9 @@
|
|||||||
"@linux-odyssey/file-graph": "*",
|
"@linux-odyssey/file-graph": "*",
|
||||||
"@linux-odyssey/utils": "*",
|
"@linux-odyssey/utils": "*",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
|
"marked": "^10.0.0",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const handleChange = () => {
|
|||||||
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
||||||
>
|
>
|
||||||
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
|
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
|
||||||
<div class="text-error inline-flex justify-center items-center xl:hidden">
|
<div class="text-error inline-flex justify-center items-center md:hidden">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'circle-exclamation']"
|
:icon="['fas', 'circle-exclamation']"
|
||||||
class="px-1 text-sm"
|
class="px-1 text-sm"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
|
import MarkdownText from './MarkdownText.vue'
|
||||||
|
|
||||||
const current = ref(-1)
|
const current = ref(-1)
|
||||||
watch(
|
watch(
|
||||||
@@ -69,7 +70,7 @@ const right = () => {
|
|||||||
:key="hint"
|
:key="hint"
|
||||||
class="text-text font-xl whitespace-pre-wrap"
|
class="text-text font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
{{ hint }}
|
<MarkdownText :content="hint" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, defineProps } from 'vue'
|
||||||
|
import markdown2HTML from '../utils/markdown'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const html = computed(() => {
|
||||||
|
return markdown2HTML(props.content)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="markdown-content" v-html="html"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(ol) {
|
||||||
|
list-style: decimal;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
.markdown-content :deep(code) {
|
||||||
|
background-color: #282a33;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,8 +15,8 @@ import { DAG } from '@linux-odyssey/utils'
|
|||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
import { NodeImage } from '../img/svg.js'
|
||||||
|
|
||||||
const marginX = 200
|
const marginX = 500
|
||||||
const marginY = 65
|
const marginY = 300
|
||||||
const chartContainer = ref(null)
|
const chartContainer = ref(null)
|
||||||
let chartInstance = null
|
let chartInstance = null
|
||||||
const fullwidth = window.screen.width
|
const fullwidth = window.screen.width
|
||||||
@@ -71,9 +71,10 @@ const genOption = (nodes, edges) => ({
|
|||||||
symbol: () => {
|
symbol: () => {
|
||||||
return NodeImage
|
return NodeImage
|
||||||
},
|
},
|
||||||
symbolSize: [fullwidth / 10, fullwidth / 25],
|
symbolSize: [fullwidth / 13, fullwidth / 30],
|
||||||
roam: 'move',
|
roam: 'move',
|
||||||
zoom: 1,
|
zoom: 1.5,
|
||||||
|
center: ['120%', '30%'],
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
fontSize: fullwidth / 90,
|
fontSize: fullwidth / 90,
|
||||||
@@ -103,7 +104,7 @@ const genOption = (nodes, edges) => ({
|
|||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
return '#ADADB5'
|
return '#ADADB5'
|
||||||
}
|
}
|
||||||
return '454552'
|
return '#505050'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import MarkdownText from './MarkdownText.vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
|
|
||||||
|
const tasks = computed(() => {
|
||||||
|
return sessionStore.session.tasks.map((task) => {
|
||||||
|
const prefix = task.completed ? '✓' : '➤'
|
||||||
|
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
class: `${color} markdown-content`,
|
||||||
|
content: `${prefix} ${task.name}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -10,22 +24,18 @@ import sessionStore from '../store/session'
|
|||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
||||||
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
|
<MarkdownText
|
||||||
|
class="text-text markdown-content"
|
||||||
|
:content="sessionStore.quest.instruction"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<p id="tasks" class="text-text">
|
<p id="tasks" class="text-text">
|
||||||
<!-- Tasks: -->
|
<!-- Tasks: -->
|
||||||
任務:
|
任務:
|
||||||
</p>
|
</p>
|
||||||
<ul v-if="sessionStore.session">
|
<ul v-if="sessionStore.session">
|
||||||
<li v-for="task in sessionStore.session.tasks" :key="task.id">
|
<li v-for="task in tasks" :key="task.id">
|
||||||
<p v-if="task.completed" class="text-text-primary">
|
<MarkdownText :class="task.class" :content="task.content" />
|
||||||
<span class=""> ✓</span>
|
|
||||||
{{ task.name }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-text">
|
|
||||||
<span class="">➤</span>
|
|
||||||
{{ task.name }}
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,6 +121,6 @@ li.discovered {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.pwd {
|
a.pwd {
|
||||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { marked } from 'marked'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
export default function markdown2HTML(markdown) {
|
||||||
|
const html = marked.parse(markdown)
|
||||||
|
return DOMPurify.sanitize(html)
|
||||||
|
}
|
||||||
@@ -13,12 +13,11 @@ class SocketTerminal {
|
|||||||
disableStdin: false, // 是否应禁用输入
|
disableStdin: false, // 是否应禁用输入
|
||||||
// cursorStyle: "underline", //光标样式
|
// cursorStyle: "underline", //光标样式
|
||||||
cursorBlink: true, // 光标闪烁
|
cursorBlink: true, // 光标闪烁
|
||||||
theme: {
|
foreground: '#ECECEC', // 字体
|
||||||
foreground: '#ECECEC', // 字体
|
background: '#000000', // 背景色
|
||||||
background: '#000000', // 背景色
|
cursor: 'help', // 设置光标
|
||||||
cursor: 'help', // 设置光标
|
fontSize: 18,
|
||||||
lineHeight: 20,
|
fontWeight: 'normal',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
this.fitAddon = new FitAddon()
|
this.fitAddon = new FitAddon()
|
||||||
this.term.loadAddon(this.fitAddon)
|
this.term.loadAddon(this.fitAddon)
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ async function handleCommand(command) {
|
|||||||
|
|
||||||
async function handleResponse({ responses, hints, end }) {
|
async function handleResponse({ responses, hints, end }) {
|
||||||
if (responses) {
|
if (responses) {
|
||||||
await printResponses(responses, 60)
|
await printResponses(responses, 90)
|
||||||
}
|
}
|
||||||
if (hints) {
|
if (hints) {
|
||||||
await printHints(hints, 60)
|
await printHints(hints, 90)
|
||||||
}
|
}
|
||||||
if (end) {
|
if (end) {
|
||||||
console.log(colorize('Quest completed!', 'blue'))
|
console.log(colorize('Quest completed!', 'blue'))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const RETURN_SYMBOL = '\u21B5'
|
|||||||
|
|
||||||
// ANSI color map
|
// ANSI color map
|
||||||
const ANSI_COLORS = {
|
const ANSI_COLORS = {
|
||||||
|
default: 0,
|
||||||
black: 30,
|
black: 30,
|
||||||
red: 31,
|
red: 31,
|
||||||
green: 32,
|
green: 32,
|
||||||
@@ -16,10 +17,13 @@ const ANSI_COLORS = {
|
|||||||
white: 37,
|
white: 37,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colorCode(color) {
|
||||||
|
return `\x1b[${ANSI_COLORS[color]}m`
|
||||||
|
}
|
||||||
|
|
||||||
function colorize(content, color) {
|
function colorize(content, color) {
|
||||||
const colorCode = ANSI_COLORS[color]
|
if (!color) return content
|
||||||
if (!colorCode) return content
|
return `${colorCode(color)}${content}${colorCode('default')}`
|
||||||
return `\x1b[${colorCode}m${content}\x1b[0m`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdin.setRawMode(true)
|
process.stdin.setRawMode(true)
|
||||||
@@ -36,8 +40,25 @@ function waitForEnter() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLine(content, color) {
|
||||||
|
let result = colorCode(color)
|
||||||
|
let codeBlock = false
|
||||||
|
for (const c of content) {
|
||||||
|
switch (c) {
|
||||||
|
case '`':
|
||||||
|
result += colorCode(codeBlock ? color : 'green')
|
||||||
|
codeBlock = !codeBlock
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result += c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result + colorCode('default') + RETURN_SYMBOL
|
||||||
|
}
|
||||||
|
|
||||||
function printLine(content, delay, color) {
|
function printLine(content, delay, color) {
|
||||||
const printContent = `${colorize(content, color)}${RETURN_SYMBOL}`
|
const printContent = formatLine(content, color)
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let i = 0
|
let i = 0
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
|
|||||||
@@ -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'
|
import { model, Schema } from 'mongoose'
|
||||||
|
import stageSchema from './stage.js'
|
||||||
const responseSchema = new Schema({
|
import globalExceptionSchema from './globalException.js'
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Quest = model(
|
const Quest = model(
|
||||||
'Quest',
|
'Quest',
|
||||||
@@ -126,6 +34,10 @@ const Quest = model(
|
|||||||
type: [stageSchema],
|
type: [stageSchema],
|
||||||
required: true,
|
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,18 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const responseSchema = new Schema({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['narrative', 'dialogue'],
|
||||||
|
default: 'dialogue',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
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
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -73,3 +65,11 @@ stages:
|
|||||||
- 你成功的在終端機上顯示了 `How are you?`。
|
- 你成功的在終端機上顯示了 `How are you?`。
|
||||||
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
||||||
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
||||||
|
|
||||||
|
exceptions:
|
||||||
|
- repeatable: true
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ' '
|
||||||
|
hints:
|
||||||
|
- '!! 請勿使用全形空格 !!'
|
||||||
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ requirements:
|
|||||||
- read
|
- read
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
@@ -68,6 +60,7 @@ stages:
|
|||||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||||
|
|
||||||
- id: cat
|
- id: cat
|
||||||
|
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^cat hello.txt$
|
- ^cat hello.txt$
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- '[\uff21-\uff3a\uff41-\uff5a]'
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 請使用半形英文字母 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ' '
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 請使用半形空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
or:
|
||||||
|
- command:
|
||||||
|
- '`'
|
||||||
|
- command:
|
||||||
|
- \'
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 不需要輸入上下引號 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^echo\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `echo` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cd\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cd` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^ls\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `ls` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cat\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cat` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^touch\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `touch` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
command:
|
||||||
|
- ^rmdir$
|
||||||
|
command:
|
||||||
|
- ^rm\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `rm` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^mv\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `mv` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^mkdir\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `mkdir` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^rmdir\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `rmdir` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cp\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cp` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^[Ii1]s$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `ls` 的首字母是小寫的 `L` !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- 嗨
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '嗨~'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- folderl
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- filel
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||||
+1
-16
@@ -10,14 +10,6 @@ requirements:
|
|||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -50,13 +42,6 @@ stages:
|
|||||||
content:
|
content:
|
||||||
- 請先回到 `/home/commander/` 資料夾
|
- 請先回到 `/home/commander/` 資料夾
|
||||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||||
- condition:
|
|
||||||
command:
|
|
||||||
- ^[Ii]s$
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- '!! `ls` 的首字母是小寫的 `L`,不是 `I` !!'
|
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你應該能看到資料夾中的內容了。
|
- 你應該能看到資料夾中的內容了。
|
||||||
@@ -115,7 +100,7 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder1$
|
- ^/home/commander/folder1$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||||
|
|||||||
@@ -7,16 +7,9 @@ instruction: |
|
|||||||
|
|
||||||
在畫面的中間可以看到終端機和提示兩個區塊。
|
在畫面的中間可以看到終端機和提示兩個區塊。
|
||||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
||||||
|
!! 注意:不需要輸入上下引號 !!
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -73,6 +66,7 @@ stages:
|
|||||||
hints:
|
hints:
|
||||||
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
||||||
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
||||||
|
- ➤ 輸入 `echo finish` 來結束這一關
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
task: 結束關卡
|
task: 結束關卡
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ stages:
|
|||||||
task: 輸入 `echo help` 來朗誦咒語
|
task: 輸入 `echo help` 來朗誦咒語
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- echo [Hh]elp
|
- ^echo [Hh]elp$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -39,9 +39,9 @@ stages:
|
|||||||
- help
|
- help
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -65,9 +65,9 @@ stages:
|
|||||||
- find-scroll
|
- find-scroll
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- cat forgotten_scroll.txt
|
- ^cat forgotten_scroll.txt$
|
||||||
responses:
|
responses:
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
@@ -83,9 +83,9 @@ stages:
|
|||||||
- read-forgotten-scroll
|
- read-forgotten-scroll
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- echo [Hh]ello\s*[Ww]orld
|
- ^echo [Hh]ello\s*[Ww]orld$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
+7
-20
@@ -11,14 +11,6 @@ requirements:
|
|||||||
- explore
|
- explore
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -64,23 +56,20 @@ stages:
|
|||||||
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
||||||
|
|
||||||
- id: cd-folder2
|
- id: cd-folder2
|
||||||
task: 用指定方式進入 `folder2` 資料夾
|
task: 進入 `folder2` 資料夾
|
||||||
requirements:
|
requirements:
|
||||||
- cd-folder1
|
- cd-folder1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
command:
|
|
||||||
- ^cd \.\./folder2$
|
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
pwd:
|
||||||
pwd:
|
- ^/home/commander$
|
||||||
- ^/home/commander/folder1$
|
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請先回到 `folder1` 資料夾,並用指定的方式進入 `folder2` 資料夾。
|
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||||
@@ -95,10 +84,8 @@ stages:
|
|||||||
requirements:
|
requirements:
|
||||||
- cd-folder2
|
- cd-folder2
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
output:
|
||||||
- ^/home/commander/folder2$
|
- file1.txt
|
||||||
command:
|
|
||||||
- ^ls \.\.\/folder1$
|
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
@@ -107,7 +94,7 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請先回到 `folder2` 資料夾,並用指定的方式查看 `folder1` 資料夾的內容。
|
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 很好,你現在知道如何運用相對路徑了。
|
- 很好,你現在知道如何運用相對路徑了。
|
||||||
|
|||||||
+6
-14
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- path
|
- path
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -44,19 +36,19 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 這個資料夾中有一個檔案和一個資料夾。
|
- 這個資料夾中有一個檔案和一個資料夾。
|
||||||
- 現在你可以使用 `cat` 指令來查看 `file.txt` 的內容。
|
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `cat file.txt` 來查看檔案的內容。
|
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
|
||||||
|
|
||||||
- id: cat-file
|
- id: cat-file1
|
||||||
task: 查看 `file.txt` 的內容
|
task: 查看 `file1.txt` 的內容
|
||||||
requirements:
|
requirements:
|
||||||
- ls-~
|
- ls-~
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat file.txt$
|
- ^cat file1.txt$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你應該能看到檔案的內容了。
|
- 你應該能看到檔案的內容了。
|
||||||
@@ -68,7 +60,7 @@ stages:
|
|||||||
- id: ls-folder
|
- id: ls-folder
|
||||||
task: 查看 `folder` 資料夾
|
task: 查看 `folder` 資料夾
|
||||||
requirements:
|
requirements:
|
||||||
- cat-file
|
- cat-file1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
|
|||||||
@@ -13,14 +13,6 @@ requirements:
|
|||||||
- move
|
- move
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export function createContainer(name, questId) {
|
|||||||
Image: getQuestImage(questId),
|
Image: getQuestImage(questId),
|
||||||
}
|
}
|
||||||
const { hostPwd, mountQuest } = config.docker
|
const { hostPwd, mountQuest } = config.docker
|
||||||
if (!config.isProduction && hostPwd && mountQuest) {
|
if (!config.isProduction && hostPwd && mountQuest === questId) {
|
||||||
logger.debug('Mounting quest folder', mountQuest)
|
logger.info('Mounting quest folder', mountQuest)
|
||||||
option.HostConfig.Binds = [
|
option.HostConfig.Binds = [
|
||||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
||||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
||||||
|
|||||||
@@ -50,9 +50,18 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async discoverHandler() {
|
async discoverHandler() {
|
||||||
const graph = new FileGraph(this.session.graph)
|
try {
|
||||||
graph.discover(this.params.discover)
|
const graph = new FileGraph(this.session.graph)
|
||||||
this.session.graph = graph
|
graph.discover(this.params.discover)
|
||||||
|
this.session.graph = graph
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Update file graph failed', {
|
||||||
|
error: e.message,
|
||||||
|
session: this.session.id,
|
||||||
|
graph: this.session.graph,
|
||||||
|
discover: this.params.discover,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isMatch(condition) {
|
async isMatch(condition) {
|
||||||
@@ -111,22 +120,33 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
this.quest = await Quest.findById(this.session.quest)
|
try {
|
||||||
const stages = this.getStages()
|
this.quest = await Quest.findById(this.session.quest)
|
||||||
if (stages.length === 0) {
|
const stages = this.getStages()
|
||||||
logger.warn('stage not found', this.session.tasks)
|
if (stages.length === 0) {
|
||||||
return {}
|
logger.warn('stage not found', { session: this.session.id })
|
||||||
}
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
this.handleEvent()
|
this.handleEvent()
|
||||||
this.handleCommand()
|
this.handleCommand()
|
||||||
|
|
||||||
const matches = await Promise.all(
|
const matches = await Promise.all(
|
||||||
stages.map((stage) => this.isMatch(stage.condition))
|
stages.map((stage) => this.isMatch(stage.condition))
|
||||||
)
|
)
|
||||||
|
|
||||||
const stage = stages.find((_, i) => matches[i])
|
const stage = stages.find((_, i) => matches[i])
|
||||||
if (!stage) {
|
if (stage) {
|
||||||
|
const response = this.execute(stage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
stage: stage.id,
|
||||||
|
end: this.session.status === 'finished',
|
||||||
|
...response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage exception
|
||||||
for (const s of stages) {
|
for (const s of stages) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const exception = await this.checkException(s)
|
const exception = await this.checkException(s)
|
||||||
@@ -134,14 +154,29 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
return this.executeException(exception)
|
return this.executeException(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const response = this.execute(stage)
|
|
||||||
|
|
||||||
return {
|
// global exception
|
||||||
stage: stage.id,
|
if (this.quest.exceptions.length > 0) {
|
||||||
end: this.session.status === 'finished',
|
const globalExceptions = this.getUnlockedComponents(
|
||||||
...response,
|
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.message,
|
||||||
|
session: this.session._id,
|
||||||
|
quest: this.quest._id,
|
||||||
|
command: this.commandInput,
|
||||||
|
})
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,14 +19,17 @@ export default class SessionHandler {
|
|||||||
return task ? task.completed : false
|
return task ? task.completed : false
|
||||||
}
|
}
|
||||||
|
|
||||||
getStages() {
|
getUnlockedComponents(components) {
|
||||||
return this.quest.stages.filter(
|
return components.filter((component) =>
|
||||||
(stage) =>
|
component.requirements.every((r) => this.isCompleted({ id: r }))
|
||||||
stage.requirements.every((r) => this.isCompleted({ id: r })) &&
|
|
||||||
(stage.repeatable || !this.isCompleted(stage))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStages() {
|
||||||
|
const unlockedStages = this.getUnlockedComponents(this.quest.stages)
|
||||||
|
return unlockedStages.filter((s) => !this.isCompleted(s))
|
||||||
|
}
|
||||||
|
|
||||||
getNewTasks() {
|
getNewTasks() {
|
||||||
const stages = this.getStages()
|
const stages = this.getStages()
|
||||||
return stages
|
return stages
|
||||||
|
|||||||
+6
-1
@@ -38,7 +38,12 @@ async function main() {
|
|||||||
await setupTest()
|
await setupTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadAndUpdateQuests()
|
try {
|
||||||
|
await loadAndUpdateQuests()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load quests', err)
|
||||||
|
return
|
||||||
|
}
|
||||||
expiryRemovalScheduler()
|
expiryRemovalScheduler()
|
||||||
|
|
||||||
const file = await fs.readFile('./swagger.yaml', 'utf8')
|
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'
|
import config from '../config.js'
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
format: winston.format.json(),
|
format: format.combine(
|
||||||
|
format.errors({ stack: true }),
|
||||||
|
format.timestamp(),
|
||||||
|
format.json()
|
||||||
|
),
|
||||||
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
||||||
transports: [
|
transports: [
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import logger from './logger.js'
|
|||||||
|
|
||||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
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) {
|
async function mapQuests(callback) {
|
||||||
const questNames = await fs.readdir(questDirectory, {
|
const questNames = await fs.readdir(questDirectory, {
|
||||||
withFileTypes: true,
|
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() {
|
export async function loadAndUpdateQuests() {
|
||||||
await Quest.deleteMany({})
|
await Quest.deleteMany({})
|
||||||
|
const { exceptions } = await loadGlobalExceptions()
|
||||||
|
console.log(exceptions)
|
||||||
return mapQuests(async (id, questPath) => {
|
return mapQuests(async (id, questPath) => {
|
||||||
const files = await fs.readdir(questPath)
|
const files = await fs.readdir(questPath)
|
||||||
if (!files.includes('game.yml')) {
|
if (!files.includes('game.yml')) {
|
||||||
throw new Error(`Quest ${id} is missing game.yml`)
|
throw new QuestValidationError(`Missing game.yml`, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -44,10 +61,12 @@ export async function loadAndUpdateQuests() {
|
|||||||
image,
|
image,
|
||||||
...questData,
|
...questData,
|
||||||
})
|
})
|
||||||
|
quest.exceptions.push(...exceptions)
|
||||||
await quest.save()
|
await quest.save()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error parsing quest ${id}:`, error)
|
console.error(error)
|
||||||
throw error
|
logger.error(`Error parsing quest`, { id, error })
|
||||||
|
throw new QuestValidationError(`Error parsing quest`, id, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2404,6 +2404,11 @@ dom-accessibility-api@^0.5.9:
|
|||||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||||
|
|
||||||
|
dompurify@^3.0.6:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae"
|
||||||
|
integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==
|
||||||
|
|
||||||
dotenv@^16.3.1:
|
dotenv@^16.3.1:
|
||||||
version "16.3.1"
|
version "16.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||||
@@ -4691,6 +4696,11 @@ makeerror@1.0.12:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tmpl "1.0.5"
|
tmpl "1.0.5"
|
||||||
|
|
||||||
|
marked@^10.0.0:
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-10.0.0.tgz#7fe1805bb908433d760e2de0fcc8841a2b2d745c"
|
||||||
|
integrity sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
|
|||||||
Reference in new issue
Block a user