mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
fix typescript errors and update dependencies
This commit is contained in:
Binary file not shown.
@@ -72,7 +72,7 @@ const scheduleHotjarInitialization = () => {
|
||||
hotjarInitialized.value = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
initialize(HOTJAR_ID, HOTJAR_SCRIPT_VERSION);
|
||||
initialize();
|
||||
}, HOTJAR_INIT_DELAY);
|
||||
};
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ import type {
|
||||
DecisionNodeModel,
|
||||
DecisionNodeTransition,
|
||||
DecisionNodeLayout,
|
||||
} from '~/shared/types/decision'
|
||||
} from '~~/shared/types/decision'
|
||||
|
||||
const NODE_WIDTH = 280
|
||||
const NODE_HEIGHT = 160
|
||||
@@ -403,14 +403,22 @@ const preparedNodes = computed(() => {
|
||||
const width = node.layout?.width ?? NODE_WIDTH
|
||||
const height = node.layout?.height ?? NODE_HEIGHT
|
||||
const accent = node.layout?.color || props.roleColors[node.role] || '#22d3ee'
|
||||
const transitions = (node.model.transitions || []).slice(0, 3).map((transition) => ({
|
||||
const transitions = (node.model.transitions || []).slice(0, 3).map((transition: DecisionNodeTransition) => ({
|
||||
key: transition.key || `${node.id}_${transition.target}_${transition.type}`,
|
||||
target: transition.target,
|
||||
type: transition.type,
|
||||
class: transitionClass(transition),
|
||||
}))
|
||||
|
||||
const baseLayout: DecisionNodeLayout = { ...(node.layout || {}) }
|
||||
const baseLayout: DecisionNodeLayout = {
|
||||
x: node.layout?.x ?? 0,
|
||||
y: node.layout?.y ?? 0,
|
||||
width: node.layout?.width,
|
||||
height: node.layout?.height,
|
||||
color: node.layout?.color,
|
||||
icon: node.layout?.icon,
|
||||
locked: node.layout?.locked,
|
||||
}
|
||||
const displayLayout: DecisionNodeLayout = {
|
||||
...baseLayout,
|
||||
x: (baseLayout.x ?? 0) + offset.x,
|
||||
@@ -430,6 +438,8 @@ const preparedNodes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
type PreparedNode = typeof preparedNodes.value[number]
|
||||
|
||||
const canvasBounds = computed(() => {
|
||||
const defaultWidth = MIN_WORKSPACE_WIDTH - WORKSPACE_PADDING
|
||||
const defaultHeight = MIN_WORKSPACE_HEIGHT - WORKSPACE_PADDING
|
||||
@@ -533,7 +543,7 @@ const edges = computed<EdgeDefinition[]>(() => {
|
||||
})
|
||||
const color = transitionColor(transition)
|
||||
const dashed = Boolean(transition.autoTrigger || transition.type === 'auto')
|
||||
const highlighted = node.selected || source.highlighted
|
||||
const highlighted = Boolean(node.selected || source.highlighted)
|
||||
const key = transition.key || `${node.id}_${transition.target}_${transition.type}`
|
||||
|
||||
lines.push({
|
||||
@@ -715,11 +725,12 @@ function buildSmoothPath(points: PathPoint[], radius = 32) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const commands: string[] = [`M ${points[0].x} ${points[0].y}`]
|
||||
const first = points[0]!
|
||||
const commands: string[] = [`M ${first.x} ${first.y}`]
|
||||
|
||||
for (let i = 1; i < points.length; i += 1) {
|
||||
const prev = points[i - 1]
|
||||
const curr = points[i]
|
||||
const prev = points[i - 1]!
|
||||
const curr = points[i]!
|
||||
const next = points[i + 1]
|
||||
|
||||
if (!next) {
|
||||
@@ -799,7 +810,7 @@ interface DragState {
|
||||
|
||||
let dragState: DragState | null = null
|
||||
|
||||
function onNodePointerDown(event: PointerEvent, node: ReturnType<typeof preparedNodes.value[number]>) {
|
||||
function onNodePointerDown(event: PointerEvent, node: PreparedNode) {
|
||||
if (event.button !== 0) return
|
||||
emit('select', node.id)
|
||||
emit('node-drag-start', node.id)
|
||||
@@ -926,7 +937,7 @@ function onWheel(event: WheelEvent) {
|
||||
emit('update:pan', { x: nextPanX, y: nextPanY })
|
||||
}
|
||||
|
||||
function nodeStyle(node: ReturnType<typeof preparedNodes.value[number]>) {
|
||||
function nodeStyle(node: PreparedNode) {
|
||||
return {
|
||||
width: `${node.width}px`,
|
||||
height: `${node.height}px`,
|
||||
|
||||
@@ -1085,9 +1085,51 @@ interface DecisionTraceCall {
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface DecisionTraceCandidate {
|
||||
id: string
|
||||
flow?: string
|
||||
summary?: string
|
||||
}
|
||||
|
||||
interface DecisionTraceEliminationContext {
|
||||
patterns?: Array<{ id?: string; pattern?: string; flags?: string }>
|
||||
condition?: {
|
||||
id?: string
|
||||
type: 'variable_value' | 'regex' | 'regex_not'
|
||||
variable?: string
|
||||
operator?: string
|
||||
value?: unknown
|
||||
pattern?: string
|
||||
patternFlags?: string
|
||||
}
|
||||
actualValue?: unknown
|
||||
expectedValue?: unknown
|
||||
}
|
||||
|
||||
interface DecisionTraceElimination {
|
||||
candidate: DecisionTraceCandidate
|
||||
kind: 'regex' | 'condition'
|
||||
reason: string
|
||||
context?: DecisionTraceEliminationContext
|
||||
}
|
||||
|
||||
interface DecisionTraceStep {
|
||||
stage: string
|
||||
label: string
|
||||
candidates: DecisionTraceCandidate[]
|
||||
eliminated?: DecisionTraceElimination[]
|
||||
note?: string
|
||||
}
|
||||
|
||||
interface DecisionTraceMetadata {
|
||||
calls?: DecisionTraceCall[]
|
||||
fallback?: { used?: boolean; reason?: string; selected?: string }
|
||||
candidateTimeline?: {
|
||||
steps: DecisionTraceStep[]
|
||||
fallbackUsed?: boolean
|
||||
autoSelected?: DecisionTraceCandidate | null
|
||||
}
|
||||
autoSelection?: { id: string; flow: string; reason?: string }
|
||||
}
|
||||
|
||||
interface CandidateSnapshot {
|
||||
@@ -1122,6 +1164,7 @@ interface TransmissionMetadata {
|
||||
controller_say_tpl?: string
|
||||
off_schema?: boolean
|
||||
radio_check?: boolean
|
||||
activate_flow?: string | { slug: string; mode?: string }
|
||||
}
|
||||
decisionTrace?: DecisionTraceMetadata
|
||||
context?: TransmissionContextSnapshot
|
||||
@@ -1410,6 +1453,32 @@ function describeTransition(transition: any) {
|
||||
return details.length ? `${destination} (${details.join(', ')})` : destination
|
||||
}
|
||||
|
||||
function describeElimination(entry: DecisionTraceElimination | null | undefined): string {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return ''
|
||||
}
|
||||
if (entry.kind === 'regex' && entry.context?.patterns?.length) {
|
||||
const patterns = entry.context.patterns
|
||||
.map((pattern) => pattern?.pattern)
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.join(', ')
|
||||
return patterns ? `Patterns: ${patterns}` : entry.reason
|
||||
}
|
||||
if (entry.kind === 'condition' && entry.context?.condition) {
|
||||
const condition = entry.context.condition
|
||||
if (condition.type === 'regex' || condition.type === 'regex_not') {
|
||||
const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : ''
|
||||
return flag ? `Condition: ${condition.type} ${flag}` : entry.reason
|
||||
}
|
||||
const variable = condition.variable || 'value'
|
||||
const operator = condition.operator || '=='
|
||||
const expected = entry.context?.expectedValue ?? condition.value ?? '—'
|
||||
const actual = entry.context?.actualValue ?? '—'
|
||||
return `${variable} ${operator} ${expected} (actual: ${actual})`
|
||||
}
|
||||
return entry.reason
|
||||
}
|
||||
|
||||
function isExpired(expiresAt?: string) {
|
||||
if (!expiresAt) return false
|
||||
const date = new Date(expiresAt)
|
||||
|
||||
@@ -1045,7 +1045,7 @@
|
||||
:key="segment.type === 'field' ? `f-${segment.key}` : `t-${idx}`">
|
||||
<span v-if="segment.type === 'text'" class="cloze-chunk cloze-text">
|
||||
{{
|
||||
displayCallsign(typeof segment.text === 'function' && scenario ? segment.text(scenario) : segment.text)
|
||||
displayCallsign(typeof segment.text === 'function' ? (scenario ? segment.text(scenario) : '') : segment.text)
|
||||
}}
|
||||
</span>
|
||||
<label
|
||||
@@ -1704,6 +1704,7 @@ type ExperienceOption = {
|
||||
description: string
|
||||
icon: string
|
||||
to: string
|
||||
target?: '_self' | '_blank'
|
||||
matches: (path: string) => boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1156,10 +1156,11 @@ import type {
|
||||
DecisionFlowSummary,
|
||||
DecisionNodeCondition,
|
||||
DecisionNodeModel,
|
||||
DecisionNodeRole,
|
||||
DecisionNodeTrigger,
|
||||
DecisionNodeTransition,
|
||||
DecisionNodeLayout,
|
||||
} from '~/shared/types/decision'
|
||||
} from '~~/shared/types/decision'
|
||||
|
||||
definePageMeta({ middleware: 'require-admin' })
|
||||
|
||||
@@ -1230,7 +1231,9 @@ const telemetryParameters = [
|
||||
const comparisonOperators = ['>', '>=', '<', '<=', '==', '!=']
|
||||
const transitionTypes: DecisionNodeTransition['type'][] = ['next', 'ok', 'bad', 'timer', 'auto', 'interrupt', 'return']
|
||||
const autoTriggerTypes = ['telemetry', 'variable', 'expression']
|
||||
const roleOptions = ['pilot', 'atc', 'system']
|
||||
const roleOptions: DecisionNodeRole[] = ['pilot', 'atc', 'system']
|
||||
const isDecisionNodeRole = (value: string): value is DecisionNodeRole =>
|
||||
roleOptions.includes(value as DecisionNodeRole)
|
||||
|
||||
const nodeTriggerTypeOptions = [
|
||||
{ value: 'auto_time', title: 'Auto (Zeit)', subtitle: 'Nach einer Verzögerung automatisch aktivieren' },
|
||||
@@ -1393,7 +1396,7 @@ let lastNodeAutosaveError = ''
|
||||
const filteredFlows = computed(() => {
|
||||
const query = String(flowSearch.value ?? '').trim().toLowerCase()
|
||||
if (!query) return flows.value
|
||||
return flows.value.filter((flow) =>
|
||||
return flows.value.filter((flow: DecisionFlowSummary) =>
|
||||
[flow.name, flow.slug, flow.description].some((entry) => entry?.toLowerCase().includes(query))
|
||||
)
|
||||
})
|
||||
@@ -1408,7 +1411,7 @@ const roleFilterOptions = computed(() => {
|
||||
const phaseFilterOptions = computed(() => {
|
||||
const phases = new Set<string>(['all'])
|
||||
flowForm.phases.forEach((phase) => phases.add(phase))
|
||||
flowDetail.value?.nodes.forEach((node) => phases.add(node.phase))
|
||||
flowDetail.value?.nodes.forEach((node: DecisionNodeModel) => phases.add(node.phase))
|
||||
return Array.from(phases)
|
||||
})
|
||||
|
||||
@@ -1420,9 +1423,9 @@ const nodeSelectorItems = computed(() => {
|
||||
const autopOnly = nodeFilter.autopOnly
|
||||
return flowDetail.value.nodes
|
||||
.slice()
|
||||
.sort((a, b) => a.stateId.localeCompare(b.stateId))
|
||||
.map((node) => {
|
||||
const autopCount = (node.transitions || []).filter((t) => t.autoTrigger).length
|
||||
.sort((a: DecisionNodeModel, b: DecisionNodeModel) => a.stateId.localeCompare(b.stateId))
|
||||
.map((node: DecisionNodeModel) => {
|
||||
const autopCount = (node.transitions || []).filter((t: DecisionNodeTransition) => t.autoTrigger).length
|
||||
return {
|
||||
id: node.stateId,
|
||||
title: node.title,
|
||||
@@ -1441,8 +1444,8 @@ const nodeSelectorItems = computed(() => {
|
||||
matchesAuto: !autopOnly || autopCount > 0,
|
||||
}
|
||||
})
|
||||
.filter((node) => node.matchesSearch && node.matchesRole && node.matchesPhase && node.matchesAuto)
|
||||
.map(({ matchesSearch, matchesRole, matchesPhase, matchesAuto, ...rest }) => rest)
|
||||
.filter((node: any) => node.matchesSearch && node.matchesRole && node.matchesPhase && node.matchesAuto)
|
||||
.map(({ matchesSearch, matchesRole, matchesPhase, matchesAuto, ...rest }: { matchesSearch: boolean; matchesRole: boolean; matchesPhase: boolean; matchesAuto: boolean; [key: string]: any }) => rest)
|
||||
})
|
||||
|
||||
const flowModeOptions: Array<{ value: DecisionFlowEntryMode; title: string; subtitle: string }> = [
|
||||
@@ -1454,13 +1457,13 @@ const flowNodeOptions = computed(() => {
|
||||
if (!flowDetail.value) return []
|
||||
const items = flowDetail.value.nodes
|
||||
.slice()
|
||||
.sort((a, b) => a.stateId.localeCompare(b.stateId))
|
||||
.map((node) => ({
|
||||
.sort((a: DecisionNodeModel, b: DecisionNodeModel) => a.stateId.localeCompare(b.stateId))
|
||||
.map((node: DecisionNodeModel) => ({
|
||||
value: node.stateId,
|
||||
title: node.title ? `${node.stateId} — ${node.title}` : node.stateId,
|
||||
}))
|
||||
|
||||
const known = new Set(items.map((item) => item.value))
|
||||
const known = new Set(items.map((item: { value: string }) => item.value))
|
||||
const extras = [flowForm.startState, ...flowForm.endStates]
|
||||
.map((state) => (typeof state === 'string' ? state.trim() : ''))
|
||||
.filter((state): state is string => Boolean(state && !known.has(state)))
|
||||
@@ -1703,8 +1706,8 @@ watch(
|
||||
if (!flowDetail.value || flowInitializing) return
|
||||
const sanitized = roles
|
||||
.map((role) => (typeof role === 'string' ? role.trim() : ''))
|
||||
.filter((role): role is string => Boolean(role))
|
||||
flowDetail.value.flow.roles = Array.from(new Set(sanitized))
|
||||
.filter((role): role is DecisionNodeRole => isDecisionNodeRole(role))
|
||||
flowDetail.value.flow.roles = Array.from(new Set<DecisionNodeRole>(sanitized))
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
@@ -1095,7 +1095,7 @@ const timelineSteps = computed(() => decisionTrace.value?.candidateTimeline?.ste
|
||||
const timelineUsedFallback = computed(() => Boolean(decisionTrace.value?.candidateTimeline?.fallbackUsed))
|
||||
const traceAutoSelection = computed(() => decisionTrace.value?.autoSelection ?? null)
|
||||
const traceFallback = computed(() => decisionTrace.value?.fallback ?? null)
|
||||
const sessionLabel = computed(() => engineSessionId.value || flags.session_id || '-')
|
||||
const sessionLabel = computed(() => engineSessionId.value || flags.value.session_id || '-')
|
||||
|
||||
const VALID_TRACE_STAGES: ReadonlySet<CandidateTraceStage> = new Set(
|
||||
[
|
||||
@@ -1130,7 +1130,22 @@ const ensureTraceCalls = (calls: unknown): LLMDecisionTrace['calls'] => {
|
||||
}
|
||||
return calls
|
||||
.filter((entry): entry is Record<string, any> => isPlainObject(entry))
|
||||
.map((entry) => cloneForTrace(entry))
|
||||
.map((entry) => {
|
||||
const normalized: LLMDecisionTrace['calls'][number] = {
|
||||
stage: entry.stage === 'readback-check' ? 'readback-check' : 'decision',
|
||||
request: isPlainObject(entry.request) ? cloneForTrace(entry.request) : {},
|
||||
}
|
||||
if ('response' in entry) {
|
||||
normalized.response = cloneForTrace(entry.response)
|
||||
}
|
||||
if (typeof entry.rawResponseText === 'string') {
|
||||
normalized.rawResponseText = entry.rawResponseText
|
||||
}
|
||||
if (typeof entry.error === 'string') {
|
||||
normalized.error = entry.error
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeTraceFallback = (raw: unknown): LLMDecisionTrace['fallback'] | undefined => {
|
||||
@@ -1799,7 +1814,7 @@ const speakPrepared = async (prepared: PreparedSpeech, options: SpeechOptions =
|
||||
moduleId: 'pilot-monitoring',
|
||||
lessonId: currentState.value?.id || 'general',
|
||||
tag: options.tag || 'controller-reply',
|
||||
sessionId: engineSessionId.value || flags.session_id || undefined,
|
||||
sessionId: engineSessionId.value || flags.value.session_id || undefined,
|
||||
})
|
||||
|
||||
if (response.success && response.audio) {
|
||||
@@ -1843,7 +1858,7 @@ const speakPlainText = (text: string, options: SpeechOptions = {}) => {
|
||||
moduleId: 'pilot-monitoring',
|
||||
lessonId,
|
||||
tag: options.tag || 'announcement',
|
||||
sessionId: engineSessionId.value || flags.session_id || undefined,
|
||||
sessionId: engineSessionId.value || flags.value.session_id || undefined,
|
||||
})
|
||||
|
||||
if (response.success && response.audio) {
|
||||
|
||||
@@ -3,6 +3,21 @@ export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: {enabled: false},
|
||||
ssr: false,
|
||||
typescript: {
|
||||
strict: false,
|
||||
typeCheck: true,
|
||||
tsConfig: {
|
||||
compilerOptions: {
|
||||
noUncheckedIndexedAccess: false,
|
||||
noImplicitOverride: false,
|
||||
lib: ['ESNext', 'dom', 'dom.iterable', 'webworker'],
|
||||
types: ['vite/client'],
|
||||
},
|
||||
vueCompilerOptions: {
|
||||
strictTemplates: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
modules: [
|
||||
'vuetify-nuxt-module',
|
||||
'@nuxtjs/tailwindcss',
|
||||
@@ -77,6 +92,17 @@ export default defineNuxtConfig({
|
||||
experimental: {
|
||||
websocket: true,
|
||||
},
|
||||
typescript: {
|
||||
strict: false,
|
||||
tsConfig: {
|
||||
compilerOptions: {
|
||||
noUncheckedIndexedAccess: false,
|
||||
noImplicitOverride: false,
|
||||
lib: ['ESNext', 'dom', 'dom.iterable', 'webworker'],
|
||||
types: ['node', 'vite/client'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: {
|
||||
provider: process.env.NUXT_IMAGE_PROVIDER || 'ipx',
|
||||
|
||||
34
package.json
34
package.json
@@ -16,29 +16,31 @@
|
||||
"test": "tsx --tsconfig tsconfig.tests.json --test server/utils/openai.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"dotenv": "^17.2.2",
|
||||
"@nuxt/image": "^2.0.0",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"nodemailer": "^6.9.13",
|
||||
"nuxt": "^4.1.1",
|
||||
"nuxt-aos": "1.2.5",
|
||||
"nodemailer": "^8.0.1",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-aos": "^1.2.6",
|
||||
"nuxt-module-hotjar": "^1.3.4",
|
||||
"nuxt-mongoose": "1.0.6",
|
||||
"openai": "^4.66.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuetify": "3.9.0-beta.1",
|
||||
"vuetify-nuxt-module": "0.18.7"
|
||||
"nuxt-mongoose": "^1.0.6",
|
||||
"openai": "^6.22.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^5.0.2",
|
||||
"vuetify": "^3.11.8",
|
||||
"vuetify-nuxt-module": "^0.19.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22.x"
|
||||
},
|
||||
"packageManager": "yarn@4.9.4",
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2",
|
||||
"tsx": "^4.15.1"
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vue-tsc": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ interface UpdatesRequestBody {
|
||||
source?: string
|
||||
}
|
||||
|
||||
type NotificationDataEntry = [string, ...unknown[]]
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<UpdatesRequestBody>(event)
|
||||
const email = body.email?.trim().toLowerCase()
|
||||
@@ -38,7 +40,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
|
||||
if (result.created) {
|
||||
const dataEntries = [
|
||||
const dataEntries: NotificationDataEntry[] = [
|
||||
['Email', email],
|
||||
['Name', name || null],
|
||||
['Source', source],
|
||||
|
||||
@@ -13,6 +13,8 @@ interface WaitlistRequestBody {
|
||||
wantsProductUpdates?: boolean
|
||||
}
|
||||
|
||||
type NotificationDataEntry = [string, ...unknown[]]
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<WaitlistRequestBody>(event)
|
||||
const email = body.email?.trim().toLowerCase()
|
||||
@@ -57,7 +59,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
|
||||
if (!previouslyWantedUpdates && updateResult.created) {
|
||||
const dataEntries = [
|
||||
const dataEntries: NotificationDataEntry[] = [
|
||||
['Email', email],
|
||||
]
|
||||
if (name) {
|
||||
@@ -107,7 +109,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const dataEntries = [
|
||||
const dataEntries: NotificationDataEntry[] = [
|
||||
['Email', email],
|
||||
]
|
||||
if (name) {
|
||||
@@ -132,4 +134,3 @@ export default defineEventHandler(async (event) => {
|
||||
joinedAt: entry.joinedAt,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await $fetch('https://data.vatsim.net/v3/vatsim-data.json')
|
||||
const fetcher = (globalThis as any).$fetch as (url: string, options?: Record<string, unknown>) => Promise<unknown>
|
||||
return await fetcher('https://data.vatsim.net/v3/vatsim-data.json')
|
||||
})
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import {defineEventHandler, getRouterParam} from 'h3'
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const {cid} = getQuery(event)
|
||||
if (!cid) throw createError({statusCode: 400, statusMessage: 'cid required'})
|
||||
const query = getQuery(event)
|
||||
const rawCid = Array.isArray(query.cid) ? query.cid[0] : query.cid
|
||||
if (rawCid === undefined || rawCid === null) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'cid required' })
|
||||
}
|
||||
const cid = String(rawCid).trim()
|
||||
if (!cid) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'cid required' })
|
||||
}
|
||||
|
||||
const url = `https://api.vatsim.net/v2/members/${encodeURIComponent(cid)}/flightplans`
|
||||
return await $fetch(url, {method: 'GET'})
|
||||
const fetcher = (globalThis as any).$fetch as (target: string, options?: Record<string, unknown>) => Promise<unknown>
|
||||
return await fetcher(url, { method: 'GET' })
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
|
||||
export interface DecisionNodeDocument
|
||||
extends mongoose.Document,
|
||||
Omit<DecisionNodeModel, 'stateId' | 'transitions'> {
|
||||
Omit<DecisionNodeModel, 'stateId' | 'transitions' | 'createdAt' | 'updatedAt'> {
|
||||
flow: mongoose.Types.ObjectId
|
||||
stateId: string
|
||||
transitions: DecisionNodeTransition[]
|
||||
@@ -175,7 +175,7 @@ const decisionNodeSchema = new mongoose.Schema<DecisionNodeDocument>(
|
||||
elseSayTemplate: { type: String },
|
||||
readbackRequired: { type: [String], default: () => [] },
|
||||
autoBehavior: { type: String },
|
||||
actions: { type: [mongoose.Schema.Types.Mixed], default: () => [] },
|
||||
actions: { type: [mongoose.Schema.Types.Mixed], default: () => [] } as any,
|
||||
handoff: {
|
||||
type: new mongoose.Schema(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DecisionFlowModel,
|
||||
DecisionFlowSummary,
|
||||
DecisionNodeModel,
|
||||
DecisionNodeRole,
|
||||
DecisionNodeTransition,
|
||||
RuntimeDecisionAutoTransition,
|
||||
RuntimeDecisionState,
|
||||
@@ -12,6 +13,10 @@ import type {
|
||||
RuntimeDecisionSystem,
|
||||
} from '~~/shared/types/decision'
|
||||
|
||||
const DECISION_NODE_ROLES: DecisionNodeRole[] = ['pilot', 'atc', 'system']
|
||||
const isDecisionNodeRole = (value: unknown): value is DecisionNodeRole =>
|
||||
typeof value === 'string' && DECISION_NODE_ROLES.includes(value as DecisionNodeRole)
|
||||
|
||||
export function serializeFlowDocument(doc: DecisionFlowDocument, nodeCount = 0): DecisionFlowModel {
|
||||
return {
|
||||
id: String(doc._id),
|
||||
@@ -25,7 +30,7 @@ export function serializeFlowDocument(doc: DecisionFlowDocument, nodeCount = 0):
|
||||
flags: doc.flags || {},
|
||||
policies: doc.policies || {},
|
||||
hooks: doc.hooks || {},
|
||||
roles: Array.isArray(doc.roles) ? doc.roles : [],
|
||||
roles: Array.isArray(doc.roles) ? doc.roles.filter(isDecisionNodeRole) : [],
|
||||
phases: Array.isArray(doc.phases) ? doc.phases : [],
|
||||
layout: doc.layout || undefined,
|
||||
metadata: doc.metadata || undefined,
|
||||
@@ -206,7 +211,7 @@ async function buildRuntimeTreeForDoc(
|
||||
flags: flowDoc.flags || {},
|
||||
policies: flowDoc.policies || {},
|
||||
hooks: flowDoc.hooks || {},
|
||||
roles: Array.isArray(flowDoc.roles) ? flowDoc.roles : [],
|
||||
roles: Array.isArray(flowDoc.roles) ? flowDoc.roles.filter(isDecisionNodeRole) : [],
|
||||
phases: Array.isArray(flowDoc.phases) ? flowDoc.phases : [],
|
||||
states,
|
||||
entry_mode: flowDoc.isMain ? 'main' : flowDoc.entryMode || 'parallel',
|
||||
|
||||
@@ -203,7 +203,8 @@ export function sanitizeAutoTrigger(raw: any): DecisionNodeAutoTrigger | undefin
|
||||
} else if (normalizedType === 'variable') {
|
||||
trigger.variable = asTrimmedString(payload.variable) ?? ''
|
||||
trigger.operator = asComparisonOperatorValue(payload.operator)
|
||||
trigger.value = asVariableValue(payload.value, '')
|
||||
const variableValue = asVariableValue(payload.value, '')
|
||||
trigger.value = typeof variableValue === 'boolean' ? String(variableValue) : variableValue
|
||||
}
|
||||
|
||||
trigger.once = asBoolean(payload.once, true)
|
||||
|
||||
@@ -42,7 +42,7 @@ const ACK = createState({
|
||||
name: 'Acknowledge',
|
||||
summary: 'Acknowledge pilot readback',
|
||||
triggers: [
|
||||
{ type: 'regex', pattern: 'roger', patternFlags: 'i' },
|
||||
{ id: 'ack-regex', type: 'regex', pattern: 'roger', patternFlags: 'i' },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ const TAXI = createState({
|
||||
name: 'Taxi clearance',
|
||||
summary: 'Pilot requesting taxi clearance',
|
||||
triggers: [
|
||||
{ type: 'regex', pattern: 'request', patternFlags: 'i' },
|
||||
{ id: 'taxi-regex', type: 'regex', pattern: 'request', patternFlags: 'i' },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ const HOLD = createState({
|
||||
name: 'Hold position',
|
||||
summary: 'Pilot requesting hold position',
|
||||
triggers: [
|
||||
{ type: 'regex', pattern: 'request', patternFlags: 'i' },
|
||||
{ id: 'hold-regex', type: 'regex', pattern: 'request', patternFlags: 'i' },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -74,7 +74,16 @@ const runtimeSystem: RuntimeDecisionSystem = {
|
||||
flows: {
|
||||
main: {
|
||||
slug: 'main',
|
||||
schema_version: '1.0',
|
||||
name: 'Main Flow',
|
||||
start_state: 'START',
|
||||
end_states: [],
|
||||
variables: {},
|
||||
flags: {},
|
||||
policies: {},
|
||||
hooks: {},
|
||||
roles: ['pilot', 'atc', 'system'],
|
||||
phases: ['ground'],
|
||||
entry_mode: 'main',
|
||||
states: {
|
||||
START,
|
||||
@@ -126,7 +135,18 @@ describe('routeDecision', () => {
|
||||
],
|
||||
}
|
||||
|
||||
const result = await routeDecision(input)
|
||||
const previousApiKey = process.env.OPENAI_API_KEY
|
||||
process.env.OPENAI_API_KEY = ''
|
||||
let result: Awaited<ReturnType<typeof routeDecision>>
|
||||
try {
|
||||
result = await routeDecision(input)
|
||||
} finally {
|
||||
if (previousApiKey === undefined) {
|
||||
delete process.env.OPENAI_API_KEY
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = previousApiKey
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(result.trace?.calls.length, 1)
|
||||
assert.ok(result.trace?.calls[0]?.error)
|
||||
@@ -135,4 +155,3 @@ describe('routeDecision', () => {
|
||||
assert.equal(result.pilot_intent ?? null, null)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -17,15 +17,6 @@ import {getServerRuntimeConfig} from './runtimeConfig'
|
||||
let openaiClient: OpenAI | null = null
|
||||
let cachedModel: string | null = null
|
||||
|
||||
import https from 'node:https'
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
keepAlive: true,
|
||||
maxSockets: 50, // bei Bedarf anpassen
|
||||
maxFreeSockets: 10,
|
||||
timeout: 0 // keine Socket-Idle-Timeouts durch Node
|
||||
})
|
||||
|
||||
function ensureOpenAI(): OpenAI {
|
||||
if (!openaiClient) {
|
||||
const {openaiKey, openaiProject, openaiBaseUrl, llmModel} = getServerRuntimeConfig()
|
||||
@@ -34,7 +25,6 @@ function ensureOpenAI(): OpenAI {
|
||||
}
|
||||
const clientOptions: ConstructorParameters<typeof OpenAI>[0] = {apiKey: openaiKey,
|
||||
defaultHeaders: { 'Connection': 'keep-alive' },
|
||||
defaultHttpAgent: httpsAgent
|
||||
}
|
||||
if (openaiProject) {
|
||||
clientOptions.project = openaiProject
|
||||
@@ -1055,7 +1045,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
|
||||
JSON.stringify(optimizedInput, null, 2),
|
||||
].join('\n')
|
||||
|
||||
const callEntry = {
|
||||
const callEntry: LLMDecisionTrace['calls'][number] = {
|
||||
stage: 'decision' as const,
|
||||
request: {
|
||||
systemPrompt,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
export async function applyRadioEffect(input: string, output: string) {
|
||||
const filter="[0:a]highpass=f=300,lowpass=f=3400,compand=attacks=0.02:decays=0.25:points=-80/-900|-70/-20|0/-10|20/-8:gain=6,volume=1.2[a];anoisesrc=color=white:amplitude=0.02[ns];[a][ns]amix=inputs=2:weights=1 0.25:duration=shortest,volume=1.0,aecho=0.6:0.7:8:0.08,acompressor=threshold=0.6:ratio=6:attack=20:release=200";
|
||||
await new Promise((res,rej)=>
|
||||
await new Promise<void>((res,rej)=>
|
||||
execFile("ffmpeg",["-y","-i",input,"-filter_complex",filter,"-ar","16000",output],
|
||||
(err,_,stderr)=>err?rej(new Error(stderr)):res())
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UpdateSubscriber, UpdateSubscriberDocument } from '../models/UpdateSubscriber'
|
||||
import { UpdateSubscriber } from '../models/UpdateSubscriber'
|
||||
import type { UpdateSubscriberDocument } from '../models/UpdateSubscriber'
|
||||
|
||||
interface RegisterSubscriberOptions {
|
||||
email: string
|
||||
|
||||
@@ -46,6 +46,7 @@ export type CandidateTraceStage =
|
||||
export interface CandidateTraceEliminationContext {
|
||||
patterns?: Array<{ id?: string; pattern?: string; flags?: string }>
|
||||
transcript?: string
|
||||
operator?: string
|
||||
condition?: {
|
||||
id?: string
|
||||
type: DecisionNodeCondition['type']
|
||||
|
||||
@@ -190,6 +190,7 @@ export default function useCommunicationsEngine() {
|
||||
stack: [],
|
||||
off_schema_count: 0,
|
||||
radio_checks_done: 0,
|
||||
session_id: '',
|
||||
})
|
||||
const currentStateId = ref<string>('')
|
||||
const communicationLog = ref<EngineLog[]>([])
|
||||
@@ -421,7 +422,7 @@ export default function useCommunicationsEngine() {
|
||||
new Set(
|
||||
entries
|
||||
.map(entry => entry?.to)
|
||||
.filter((id): id is string => typeof id === 'string' && id.length)
|
||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -546,7 +547,7 @@ export default function useCommunicationsEngine() {
|
||||
const mainFlowSlug = computed(() => runtimeSystem.value?.main || '')
|
||||
|
||||
const availableFlows = computed(() => {
|
||||
if (!runtimeSystem.value) return [] as Array<{ slug: string; name: string; description?: string; start: string }>
|
||||
if (!runtimeSystem.value) return [] as Array<{ slug: string; name: string; description?: string; start: string; mode: string }>
|
||||
return flowOrder.value
|
||||
.filter((slug) => Boolean(runtimeSystem.value!.flows[slug]))
|
||||
.map((slug) => {
|
||||
@@ -567,7 +568,7 @@ export default function useCommunicationsEngine() {
|
||||
if (typeof fetcher !== 'function') {
|
||||
throw new Error('Universal fetch is not available in this context')
|
||||
}
|
||||
const data = await fetcher<RuntimeDecisionSystem>('/api/decision-flows/runtime')
|
||||
const data = (await fetcher('/api/decision-flows/runtime')) as RuntimeDecisionSystem
|
||||
const activeSlug = slug && data.flows[slug] ? slug : data.main
|
||||
resetEngineFromSystem(data, { activeSlug })
|
||||
}
|
||||
|
||||
@@ -17,7 +17,18 @@ interface FrontMatter {
|
||||
readingTime?: string
|
||||
}
|
||||
|
||||
const rawNewsModules = import.meta.glob('~~/content/news/*.md', {
|
||||
type ImportMetaWithGlob = ImportMeta & {
|
||||
glob: (
|
||||
pattern: string,
|
||||
options?: {
|
||||
query?: string
|
||||
import?: string
|
||||
eager?: boolean
|
||||
}
|
||||
) => Record<string, unknown>
|
||||
}
|
||||
|
||||
const rawNewsModules = (import.meta as ImportMetaWithGlob).glob('~~/content/news/*.md', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
eager: true,
|
||||
|
||||
@@ -403,7 +403,7 @@ class TremoloEffect implements EffectNode {
|
||||
const frequency = Math.max(0.0001, this.oscillator.frequency.value)
|
||||
offset = Math.random() * (1 / frequency)
|
||||
}
|
||||
this.oscillator.start(0, offset)
|
||||
this.oscillator.start(this.context.currentTime + offset)
|
||||
this.started = true
|
||||
} catch {
|
||||
// oscillator already started
|
||||
|
||||
Reference in New Issue
Block a user