From 965141def2cacfcd20ab8a6b7fb8c3c9eb8b0487 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Mon, 12 May 2025 13:57:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AF=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E7=9A=84=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ktor/.gitignore | 39 + ktor/build.gradle.kts | 106 +++ ktor/gradle.properties | 8 + ktor/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes ktor/gradle/wrapper/gradle-wrapper.properties | 5 + ktor/gradlew | 234 +++++ ktor/gradlew.bat | 89 ++ ktor/package-lock.json | 6 + ktor/settings.gradle.kts | 1 + .../ink/snowflake/server/Application.kt | 65 ++ .../kotlin/ink/snowflake/server/Test.http | 24 + .../snowflake/server/model/ai/ChatRequest.kt | 21 + .../snowflake/server/model/ai/ChatResponse.kt | 37 + .../server/model/database/AIProfilesTable.kt | 44 + .../server/model/database/ChatRecordsTable.kt | 49 + .../model/database/ImageAnalyticsRequest.kt | 18 + .../server/model/database/ImageTable.kt | 24 + .../server/model/database/UserTable.kt | 29 + .../database/VideoAnalyticsDetailTable.kt | 12 + .../server/model/database/VideoTable.kt | 25 + .../server/model/request/CommonRequest.kt | 8 + .../server/model/request/DevicesInfo.kt | 74 ++ .../server/model/request/LoginRequest.kt | 6 + .../model/request/RefreshTokenRequest.kt | 7 + .../server/model/request/RegisterRequest.kt | 6 + .../model/request/VideoAnalyticsRequest.kt | 32 + .../server/model/response/AiListForUser.kt | 10 + .../server/model/response/BaseResponse.kt | 11 + .../server/model/response/DeviceItem.kt | 9 + .../server/model/response/LoginResponse.kt | 8 + .../model/response/RefreshTokenResponse.kt | 6 + .../snowflake/server/model/response/Token.kt | 6 + .../server/model/response/UserInfo.kt | 12 + .../model/response/VideoAnalyticsDetail.kt | 47 + .../model/response/VideoListResponse.kt | 10 + .../server/model/response/WSChatMessage.kt | 12 + .../ink/snowflake/server/plugins/CORS.kt | 25 + .../ink/snowflake/server/plugins/Databases.kt | 13 + .../ink/snowflake/server/plugins/Security.kt | 40 + .../snowflake/server/plugins/Serialization.kt | 20 + .../snowflake/server/plugins/StaticPath.kt | 16 + .../snowflake/server/plugins/StatusPages.kt | 87 ++ .../snowflake/server/plugins/Templating.kt | 21 + .../server/repository/ImageDataBase.kt | 32 + .../server/repository/UserDataBase.kt | 49 + .../server/repository/VideoDataBase.kt | 158 ++++ .../kotlin/ink/snowflake/server/route/Main.kt | 30 + .../server/route/configureSockets.kt | 21 + .../ink/snowflake/server/route/func/Chat.kt | 144 +++ .../server/route/func/ImageAnalytics.kt | 42 + .../server/route/func/RemoteDebug.kt | 165 ++++ .../ink/snowflake/server/route/func/User.kt | 256 +++++ .../server/route/func/VideoAnalytics.kt | 159 ++++ .../server/route/func/VideoAnalyticsJetson.kt | 187 ++++ .../ink/snowflake/server/utils/AppConfig.kt | 19 + .../ink/snowflake/server/utils/MyUtils.kt | 90 ++ .../server/utils/WebSocketManager.kt | 55 ++ .../snowflake/server/utils/database/AIDao.kt | 19 + .../server/utils/database/ChatRecordsDao.kt | 49 + .../server/utils/database/ImageDao.kt | 60 ++ .../server/utils/database/UserDAO.kt | 92 ++ .../server/utils/database/VideoDao.kt | 92 ++ ktor/src/main/resources/application.yaml | 30 + .../ink/snowflake/server/ApplicationTest.kt | 64 ++ lefthook.yml | 42 - ...timestamp-1747016591710-9e47cd4ea35768.mjs | 877 ++++++++++++++++++ 66 files changed, 4012 insertions(+), 42 deletions(-) create mode 100644 ktor/.gitignore create mode 100644 ktor/build.gradle.kts create mode 100644 ktor/gradle.properties create mode 100644 ktor/gradle/wrapper/gradle-wrapper.jar create mode 100644 ktor/gradle/wrapper/gradle-wrapper.properties create mode 100644 ktor/gradlew create mode 100644 ktor/gradlew.bat create mode 100644 ktor/package-lock.json create mode 100644 ktor/settings.gradle.kts create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/Application.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/Test.http create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatResponse.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/AIProfilesTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/ChatRecordsTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageAnalyticsRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/UserTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoAnalyticsDetailTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoTable.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/CommonRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/DevicesInfo.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/LoginRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/RefreshTokenRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/RegisterRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/request/VideoAnalyticsRequest.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/AiListForUser.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/BaseResponse.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/DeviceItem.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/LoginResponse.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/RefreshTokenResponse.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/Token.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/UserInfo.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoAnalyticsDetail.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoListResponse.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/model/response/WSChatMessage.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/CORS.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/Databases.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/Security.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/Serialization.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/StaticPath.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/StatusPages.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/plugins/Templating.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/repository/ImageDataBase.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/repository/UserDataBase.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/repository/VideoDataBase.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/configureSockets.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/Chat.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/ImageAnalytics.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/RemoteDebug.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/User.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalytics.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalyticsJetson.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/AppConfig.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/MyUtils.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/WebSocketManager.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/database/AIDao.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/database/ChatRecordsDao.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/database/ImageDao.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/database/UserDAO.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/database/VideoDao.kt create mode 100644 ktor/src/main/resources/application.yaml create mode 100644 ktor/src/test/kotlin/ink/snowflake/server/ApplicationTest.kt delete mode 100644 lefthook.yml create mode 100644 vue/docs/.vitepress/config/index.mts.timestamp-1747016591710-9e47cd4ea35768.mjs diff --git a/ktor/.gitignore b/ktor/.gitignore new file mode 100644 index 0000000..04acb8f --- /dev/null +++ b/ktor/.gitignore @@ -0,0 +1,39 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Kotlin ### +.kotlin/errors/ \ No newline at end of file diff --git a/ktor/build.gradle.kts b/ktor/build.gradle.kts new file mode 100644 index 0000000..b0c7ecd --- /dev/null +++ b/ktor/build.gradle.kts @@ -0,0 +1,106 @@ +import org.gradle.kotlin.dsl.implementation + +val kotlin_version: String by project +val logback_version: String by project +val kotlinx_html_version: String by project +val ktor_version: String by project +val exposed_version: String by project +val h2_version: String by project +val postgres_version: String by project + +plugins { + kotlin("jvm") version "2.1.0" + id("io.ktor.plugin") version "3.1.0" + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" +} + +group = "ink.snowflake" +version = "0.0.1" + +application { + mainClass.set("io.ktor.server.tomcat.jakarta.EngineMain") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + google() + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } +} +ktor { + docker { + // ... + jreVersion.set(JavaVersion.VERSION_21) + localImageName.set("IVA-Ktor") + imageTag.set("0.0.1-preview") + portMappings.set(listOf( + io.ktor.plugin.features.DockerPortMapping( + 8089, + 8089, + io.ktor.plugin.features.DockerPortMappingProtocol.TCP + ) + )) + } +} +dependencies { + // 大文件传输 + implementation("io.ktor:ktor-server-partial-content-jvm") + implementation("io.ktor:ktor-server-auto-head-response-jvm") + // security + implementation("io.ktor:ktor-server-auth-jvm") + implementation("io.ktor:ktor-server-auth-jwt-jvm") + // 后端核心 + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-server-tomcat-jakarta-jvm") + implementation("io.ktor:ktor-server-host-common-jvm") + implementation("io.ktor:ktor-server-cors:$ktor_version") + // WebSocket + implementation("io.ktor:ktor-server-websockets") + // 配置页 + implementation("io.ktor:ktor-server-config-yaml") + // 序列化 + implementation("io.ktor:ktor-serialization-gson-jvm") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") + // web + implementation("io.ktor:ktor-server-thymeleaf-jvm") + implementation("io.ktor:ktor-server-html-builder-jvm") + implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.129-kotlin-1.4.20") + implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinx_html_version") + // 测试 + testImplementation("io.ktor:ktor-server-test-host-jvm") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + // 表头自适应 + implementation("io.ktor:ktor-server-content-negotiation-jvm") + // 状态页 + implementation("io.ktor:ktor-server-status-pages:$ktor_version") + // 数据库 + implementation("org.jetbrains.exposed:exposed-core:$exposed_version") + implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") + implementation("org.jetbrains.exposed:exposed-json:$exposed_version") + // MySQL JDBC +// implementation("mysql:mysql-connector-java:8.0.33") + // PostgreSQL JDBC + implementation("org.postgresql:postgresql:$postgres_version") + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version") + + implementation("ch.qos.logback:logback-classic:$logback_version") + // Radis + implementation("org.redisson:redisson:3.38.1") + + // 用于邮件发送 + implementation("javax.mail:javax.mail-api:1.6.2") + // 邮件协议实现 + implementation("com.sun.mail:javax.mail:1.6.2") + + // Ktor Client + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-cio:$ktor_version") + implementation("io.ktor:ktor-server-auth-jvm") + implementation("io.ktor:ktor-client-auth:$ktor_version") + implementation("io.ktor:ktor-client-serialization:$ktor_version") + // 数据库迁移 +// implementation("org.flywaydb:flyway-core:10.13.0") +} diff --git a/ktor/gradle.properties b/ktor/gradle.properties new file mode 100644 index 0000000..8bc11a7 --- /dev/null +++ b/ktor/gradle.properties @@ -0,0 +1,8 @@ +kotlin.code.style=official +ktor_version=3.0.2 +kotlin_version=2.1.0 +logback_version=1.4.14 +kotlinx_html_version=0.10.1 +exposed_version=0.57.0 +h2_version=2.1.214 +postgres_version=42.7.4 \ No newline at end of file diff --git a/ktor/gradle/wrapper/gradle-wrapper.jar b/ktor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/ktor/gradle/wrapper/gradle-wrapper.properties b/ktor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..39c8893 --- /dev/null +++ b/ktor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ktor/gradlew b/ktor/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/ktor/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/ktor/gradlew.bat b/ktor/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/ktor/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ktor/package-lock.json b/ktor/package-lock.json new file mode 100644 index 0000000..8db1620 --- /dev/null +++ b/ktor/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "ktor", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/ktor/settings.gradle.kts b/ktor/settings.gradle.kts new file mode 100644 index 0000000..f33a5bb --- /dev/null +++ b/ktor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ktor" diff --git a/ktor/src/main/kotlin/ink/snowflake/server/Application.kt b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt new file mode 100644 index 0000000..d98371b --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt @@ -0,0 +1,65 @@ +package ink.snowflake.server + +import com.google.gson.Gson +import ink.snowflake.server.plugins.* +import ink.snowflake.server.route.func.User +import ink.snowflake.server.route.func.chat +import ink.snowflake.server.route.configureSockets +import ink.snowflake.server.route.func.ImageAnalytics +import ink.snowflake.server.route.func.RemoteDebug +import ink.snowflake.server.route.func.VideoAnalytics +import ink.snowflake.server.route.func.VideoAnalyticsJetson +import ink.snowflake.server.route.mainFunc +import ink.snowflake.server.utils.AppConfig +import io.ktor.server.application.* +import io.ktor.server.tomcat.jakarta.* + + +const val VIDEO_INPUT_PATH = "/tmp/" + +/** + * 服务器地址 + */ +const val SERVER_PATH = "171.212.101.201" +//const val SERVER_PATH = "localhost" + +val gson = Gson() + +fun main(args: Array): Unit = EngineMain.main(args) + +fun Application.module() { + // 使用 appConfig 进行配置 + val appConfig = AppConfig(environment.config) + + // 序列化 + configureSerialization() + // Thymeleaf + configureTemplating() + // 设置-身份验证 + configureSecurity(appConfig) + // 路径 + configureStaticPath() + // 跨域 + configureCORS() + // 设置数据库 + configureDatabases(appConfig) + // 状态拦截 + configureStatusPages() + // 设置-WebSocket + configureSockets() + + // 业务-首页导航 + mainFunc() + // 业务-用户信息相关操作 + User(appConfig) + // 业务-聊天 + chat() + // 业务-远程控制 + RemoteDebug() + // 业务-视频分析 + VideoAnalytics() + // 业务-视频分析-Jetson本地 + VideoAnalyticsJetson() + // 业务-图片分析 + ImageAnalytics() +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/Test.http b/ktor/src/main/kotlin/ink/snowflake/server/Test.http new file mode 100644 index 0000000..5e77bea --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/Test.http @@ -0,0 +1,24 @@ + +### 200 首页 + +GET http://127.0.0.1:80 + +### 200 正常返回 + +GET http://127.0.0.1:80/tasks + +### 404 找不到 + +GET http://127.0.0.1:80/tasks/byPriority/Vital + +### 400 格式错误 + +GET http://127.0.0.1:80/tasks/byPriority/Mediu + +### + +GET http://0.0.0.0:8080/tasks/byPriority/Medium +Accept: application/json + +### +DELETE http://0.0.0.0:8080/tasks/delByName/gardening \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatRequest.kt new file mode 100644 index 0000000..48da55a --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatRequest.kt @@ -0,0 +1,21 @@ +package ink.snowflake.server.model.ai + +import com.google.gson.annotations.SerializedName + +data class ChatRequest( + @SerializedName("messages") + val messages: List = listOf(), + @SerializedName("model") + val model: String = "llama3.2", + @SerializedName("stream") + val stream: Boolean = false +) { + data class Message( + @SerializedName("content") + val content: String = "", + @SerializedName("role") + val role: String = "user" + ) +} + + diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatResponse.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatResponse.kt new file mode 100644 index 0000000..3742bbc --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/ai/ChatResponse.kt @@ -0,0 +1,37 @@ +package ink.snowflake.server.model.ai + +import com.google.gson.annotations.SerializedName + +data class ChatResponse( + @SerializedName("created_at") + val createdAt: String = "", + @SerializedName("done") + val done: Boolean = false, + @SerializedName("done_reason") + val doneReason: String = "", + @SerializedName("eval_count") + val evalCount: Int = 0, + @SerializedName("eval_duration") + val evalDuration: Int = 0, + @SerializedName("load_duration") + val loadDuration: Int = 0, + @SerializedName("message") + val message: Message = Message(), + @SerializedName("model") + val model: String = "", + @SerializedName("prompt_eval_count") + val promptEvalCount: Int = 0, + @SerializedName("prompt_eval_duration") + val promptEvalDuration: Int = 0, + @SerializedName("total_duration") + val totalDuration: Int = 0 +) { + data class Message( + @SerializedName("content") + val content: String = "", + @SerializedName("role") + val role: String = "" + ) +} + + diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/AIProfilesTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/AIProfilesTable.kt new file mode 100644 index 0000000..7c3eab9 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/AIProfilesTable.kt @@ -0,0 +1,44 @@ +package ink.snowflake.server.model.database + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.json.json +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import com.google.gson.reflect.TypeToken +import ink.snowflake.server.gson + +// 定义 AI 表 +object AIProfilesTable : IntIdTable("ai_profiles") { + val name = varchar("name", 100) // AI 名称 + val avatarUrl = varchar("avatar_url", 255).nullable() // AI 头像 URL + val aiPersonality = json("ai_personality", serialize = { gson.toJson(it) }, deserialize = { + gson.fromJson(it, object : TypeToken>() {}.type) + }) // 存储 AI 人设,使用 JSON 类型 + val memoryEnabled = bool("memory_enabled").default(true) // 是否启用记忆功能 + val creationDate = timestamp("creation_date") + val isActive = bool("is_active").default(true) // 是否激活 +} + +@Serializable +data class AIProfile( + val id: Int, + val name: String, + val avatarUrl: String?, + val aiPersonality: Map, + val memoryEnabled: Boolean, + val creationDate: String, + val isActive: Boolean +) + +fun ResultRow.toAIProfile(): AIProfile { + return AIProfile( + id = this[AIProfilesTable.id].value, + name = this[AIProfilesTable.name], + avatarUrl = this[AIProfilesTable.avatarUrl], + aiPersonality = this[AIProfilesTable.aiPersonality] as Map, + memoryEnabled = this[AIProfilesTable.memoryEnabled], + creationDate = this[AIProfilesTable.creationDate].toString(), + isActive = this[AIProfilesTable.isActive] + ) +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/ChatRecordsTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ChatRecordsTable.kt new file mode 100644 index 0000000..4e18a0b --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ChatRecordsTable.kt @@ -0,0 +1,49 @@ +package ink.snowflake.server.model.database + +import kotlinx.datetime.LocalDateTime +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.ResultRow + +object ChatRecordsTable : IntIdTable("chat_records") { + // 用户 ID(外键) + val userId: Column = integer("user_id").references(UserTable.id) + + // 消息内容 + val message: Column = text("message") + + // 消息类型 + val messageType: Column = varchar("message_type", 20) + + // 发送时间 + val sentAt = timestamp("sent_at") + + // 消息来源 + val from: Column = varchar("from", 20) + + // 情绪分析 + val emotion: Column = varchar("emotion", 50).nullable() +} + +data class ChatRecord( + val id: Int, + val userId: Int, + val message: String, + val messageType: String, + val sentAt: String,// eg. "2024-12-10T06:19:04.433006Z" + val from: String, + val emotion: String? +) + +fun ResultRow.toChatRecord(): ChatRecord { + return ChatRecord( + id = this[ChatRecordsTable.id].value, + userId = this[ChatRecordsTable.userId], + message = this[ChatRecordsTable.message], + messageType = this[ChatRecordsTable.messageType], + sentAt = this[ChatRecordsTable.sentAt].toString(), + from = this[ChatRecordsTable.from], + emotion = this[ChatRecordsTable.emotion] + ) +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageAnalyticsRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageAnalyticsRequest.kt new file mode 100644 index 0000000..f9dfde5 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageAnalyticsRequest.kt @@ -0,0 +1,18 @@ +package ink.snowflake.server.model.database + +import kotlinx.serialization.Serializable + +@Serializable +data class ImageAnalyticsRequest( + val object_name: String, // Minio存储名 + val upload_datetime: String, // 上传时间 + val file_name: String, // 文件名 + val resolution: String, // 图片分辨率 + val size: Float, // 文件大小,单位MB + val cocoon_count: Float, // 识别出的茧数量 + val max_confidence: Float, // 最大置信度 + val min_confidence: Float, // 最小置信度 + val average_confidence: Float, // 平均置信度 + val other_info: Map, // 额外信息 + val processing_time: String // 处理时间 +) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageTable.kt new file mode 100644 index 0000000..7ddd4c9 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/ImageTable.kt @@ -0,0 +1,24 @@ +package ink.snowflake.server.model.database + +import com.google.gson.reflect.TypeToken +import ink.snowflake.server.gson +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.json.json +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + +object ImageTable : IntIdTable("image") { + val object_name = varchar("object_name", 255) + val upload_datetime = datetime("upload_datetime") + val file_name = varchar("file_name", 255) + val resolution = varchar("resolution", 255) + val size = float("size") + val cocoon_count = float("cocoon_count") + val max_confidence = float("max_confidence") + val min_confidence = float("min_confidence") + val average_confidence = float("average_confidence") + val other_info = json("other_info", serialize = { gson.toJson(it) }, deserialize = { + gson.fromJson(it, object : TypeToken>() {}.type) + }) + val processing_time = datetime("processing_time") +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/UserTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/UserTable.kt new file mode 100644 index 0000000..5630f5a --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/UserTable.kt @@ -0,0 +1,29 @@ +package ink.snowflake.server.model.database +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import java.time.LocalDateTime + + +object UserTable : IntIdTable("users") { + // 用户名 + val username = varchar("username", length = 50).nullable() + + // 邮箱 + val email = varchar("email", length = 32).nullable() + + // 手机号 + val phone = varchar("phone", length = 32).nullable() + + // 密码哈希 + val passwordHash= varchar("password_hash", length = 255) + + // 用户是否激活 + val isActive= bool("is_active").default(true) + + // 创建时间 + val createdAt = timestamp("created_at").nullable() + + // 更新时间 + val updatedAt = timestamp("updated_at").nullable() +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoAnalyticsDetailTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoAnalyticsDetailTable.kt new file mode 100644 index 0000000..ec89fa5 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoAnalyticsDetailTable.kt @@ -0,0 +1,12 @@ +package ink.snowflake.server.model.database + +import org.jetbrains.exposed.dao.id.IntIdTable + + +object VideoAnalyticsDetailTable : IntIdTable("video_analytics_detail") { + val vId = integer("v_id").references(VideoTable.id) + val aTimeStamp = integer("a_time_stamp") + val aTotalPeople = integer("a_total_people") + val aTotalPeopleMasked = integer("a_total_people_masked") + val aAction = varchar("a_action", 50) +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoTable.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoTable.kt new file mode 100644 index 0000000..15c73cd --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/database/VideoTable.kt @@ -0,0 +1,25 @@ +package ink.snowflake.server.model.database + +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + + +object VideoTable : IntIdTable("video") { + val vName = varchar("v_name", 255) + val vObjectName = varchar("v_object_name", 255) + val vFileName = varchar("v_file_name", 255) + val vStartDateTime = datetime("v_start_datetime").nullable() + val vDuration = float("v_duration") + val vSize = float("v_size") + val vVideoCodec = varchar("v_video_codec", 50) + val vAudioCodec = varchar("v_audio_codec", 50) + val vOverallBitRate = integer("v_overall_bit_rate") + val vResolution = varchar("v_resolution", 20) + val vATime = datetime("v_a_time") + val vATotalPeople = integer("v_a_total_people") + val vACountPeople = integer("v_a_count_people") + val vAMaxStayTime = float("v_a_max_stay_time") + val vAMaxAction = varchar("v_a_max_action", 50) + val vAAverageMaskedRatio = float("v_a_average_masked_ratio") +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/CommonRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/CommonRequest.kt new file mode 100644 index 0000000..e5a9609 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/CommonRequest.kt @@ -0,0 +1,8 @@ +package ink.snowflake.server.model.request + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + + +@Serializable +data class CommonRequest(@SerializedName("str") val str: String) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/DevicesInfo.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/DevicesInfo.kt new file mode 100644 index 0000000..350db49 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/DevicesInfo.kt @@ -0,0 +1,74 @@ +package ink.snowflake.server.model.request + +import com.google.gson.annotations.SerializedName + +data class DevicesInfo( + @SerializedName("proxies") + val proxies: List = listOf() +) { + data class Proxy( + @SerializedName("clientVersion") + val clientVersion: String? = null, + @SerializedName("conf") + val conf: Conf? = null, + @SerializedName("curConns") + val curConns: Int = 0, + @SerializedName("lastCloseTime") + val lastCloseTime: String = "", + @SerializedName("lastStartTime") + val lastStartTime: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("status") + val status: String = "", + @SerializedName("todayTrafficIn") + val todayTrafficIn: Long = 0, + @SerializedName("todayTrafficOut") + val todayTrafficOut: Long = 0 + ) { + data class Conf( + @SerializedName("healthCheck") + val healthCheck: HealthCheck = HealthCheck(), + @SerializedName("loadBalancer") + val loadBalancer: LoadBalancer = LoadBalancer(), + @SerializedName("localIP") + val localIP: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("plugin") + val plugin: Plugin = Plugin(), + @SerializedName("remotePort") + val remotePort: Int = 0, + @SerializedName("transport") + val transport: Transport = Transport(), + @SerializedName("type") + val type: String = "" + ) { + data class HealthCheck( + @SerializedName("intervalSeconds") + val intervalSeconds: Int = 0, + @SerializedName("type") + val type: String = "" + ) + + data class LoadBalancer( + @SerializedName("group") + val group: String = "" + ) + + data class Plugin( + @SerializedName("ClientPluginOptions") + val clientPluginOptions: Any? = null, + @SerializedName("type") + val type: String = "" + ) + + data class Transport( + @SerializedName("bandwidthLimit") + val bandwidthLimit: String = "", + @SerializedName("bandwidthLimitMode") + val bandwidthLimitMode: String = "" + ) + } + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/LoginRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/LoginRequest.kt new file mode 100644 index 0000000..e0c433d --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/LoginRequest.kt @@ -0,0 +1,6 @@ +package ink.snowflake.server.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginRequest(val account: String, val password: String) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/RefreshTokenRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/RefreshTokenRequest.kt new file mode 100644 index 0000000..578ec0e --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/RefreshTokenRequest.kt @@ -0,0 +1,7 @@ +package ink.snowflake.server.model.request + +import ink.snowflake.server.model.response.Token +import kotlinx.serialization.Serializable + +@Serializable +data class RefreshTokenRequest(val refreshToken: String) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/RegisterRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/RegisterRequest.kt new file mode 100644 index 0000000..653369d --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/RegisterRequest.kt @@ -0,0 +1,6 @@ +package ink.snowflake.server.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterRequest(var account: String, var code: String, var password: String) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/request/VideoAnalyticsRequest.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/request/VideoAnalyticsRequest.kt new file mode 100644 index 0000000..19c2cd2 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/request/VideoAnalyticsRequest.kt @@ -0,0 +1,32 @@ +package ink.snowflake.server.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class VideoAnalyticsRequest( + val v_name: String, // 项目名 + val v_object_name: String, // Minio存储名 + val v_start_datetime: String, // 开始时间 + val v_file_name: String, // 文件名 + val v_duration: Float, // 视频时长 + val v_size: Float, // 文件大小,单位MB + val v_video_codec: String, // 视频编码格式 + val v_audio_codec: String, // 音频编码格式 + val v_overall_bit_rate: Int, // 总比特率 + val v_resolution: String, // 视频分辨率 + val v_a_time: String, // 分析时间 + val v_a_total_people: Int, // 总人数 + val v_a_count_people: Int, // 累计出现人数 + val v_a_max_stay_time: Float, // 最大停留时间 + val v_a_max_action: String, // 最常见的动作 + val v_a_average_masked_ratio: Float, // 平均佩戴口罩比率 + val v_a_details: List // 详细记录 +) + +@Serializable +data class VideoDetail( + val a_time_stamp: Int, // 时间戳 + val a_total_people: Int, // 出现的总人数 + val a_total_people_masked: Int, // 佩戴口罩的人数 + val a_action: String // 动作类型 +) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/AiListForUser.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/AiListForUser.kt new file mode 100644 index 0000000..fff9fbe --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/AiListForUser.kt @@ -0,0 +1,10 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class AiListForUser( + val aiId :Int, + val aiName :String, + val aiAvatar : String +) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/BaseResponse.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/BaseResponse.kt new file mode 100644 index 0000000..946d149 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/BaseResponse.kt @@ -0,0 +1,11 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +open class BaseResponse( + val status: Boolean = true, + val message: String = if(status) "操作成功" else "操作失败", + val data: T? = null, +) + diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/DeviceItem.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/DeviceItem.kt new file mode 100644 index 0000000..b4485e5 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/DeviceItem.kt @@ -0,0 +1,9 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class DeviceItem ( + val deviceName:String, + val devicePort:Int, +) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/LoginResponse.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/LoginResponse.kt new file mode 100644 index 0000000..10971fe --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/LoginResponse.kt @@ -0,0 +1,8 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + + +@Serializable +data class LoginResponse(val userId: String, val accessToken: Token, val refreshToken: Token) + diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/RefreshTokenResponse.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/RefreshTokenResponse.kt new file mode 100644 index 0000000..9b3d39d --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/RefreshTokenResponse.kt @@ -0,0 +1,6 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RefreshTokenResponse(val accessToken: Token, val refreshToken: Token) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/Token.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/Token.kt new file mode 100644 index 0000000..d4ced83 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/Token.kt @@ -0,0 +1,6 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class Token(val token: String, val expiresAt: Long = 0,val expiresStr:String) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/UserInfo.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/UserInfo.kt new file mode 100644 index 0000000..6f6c05b --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/UserInfo.kt @@ -0,0 +1,12 @@ +package ink.snowflake.server.model.response + +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val username: String?, + val email: String?, + val phone: String?, + val roles: List = emptyList() +) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoAnalyticsDetail.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoAnalyticsDetail.kt new file mode 100644 index 0000000..3d3b22f --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoAnalyticsDetail.kt @@ -0,0 +1,47 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class VideoAnalyticsDetail( + val v_name: String, + val v_video_play_path: String, + val v_file_name: String, + val v_duration: Float, + val v_size: Float, + val v_start_datetime: String, + val v_video_codec: String, + val v_audio_codec: String, + val v_overall_bit_rate: Int, + val v_resolution: String, + val v_a_time: String, + val v_a_total_people: Int, + val v_a_count_people: Int, + val v_a_max_stay_time: Float, + val v_a_max_action: String, + val v_a_average_masked_ratio: Float, + val v_a_details: VideoAnalyticsData, + val v_details_list: List +) + +@Serializable +data class VideoAnalyticsData( + val yTotalData: List>, // 总人数数据 (时间, 总人数) + val yMaskedData: List>, // 佩戴口罩人数数据 (时间, 佩戴人数) + val areaData: List> // 区域数据,使用 Area 对象 +) +@Serializable +data class Area( + val xAxis: String, + val itemStyle: ItemStyle +) +@Serializable +data class DetailItem( + val action: String, + val time: String +) + +@Serializable +data class ItemStyle( + val color: String +) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoListResponse.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoListResponse.kt new file mode 100644 index 0000000..49bade8 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/VideoListResponse.kt @@ -0,0 +1,10 @@ +package ink.snowflake.server.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class VideoListResponse( + val v_id: Int, + val v_name: String, + val v_a_time: String? +) \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/model/response/WSChatMessage.kt b/ktor/src/main/kotlin/ink/snowflake/server/model/response/WSChatMessage.kt new file mode 100644 index 0000000..cb7893c --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/model/response/WSChatMessage.kt @@ -0,0 +1,12 @@ +package com.cyberecho.mdoel.database + + +data class WSChatRecords( + val id: Long = 0, // 消息的唯一标识符,自增 + val aiId: Int, // 交流的对象AI_ID + val messageType: String,// 消息类型(文本、图片、语音等) + val content: String, // 消息内容 + val timestamp: Long, // 发送时间(时间戳,单位为毫秒) + val status: Int = 0, // 消息状态 默认为未读0 + val msgFrom: String // 消息来源(AI 或 用户) +) diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/CORS.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/CORS.kt new file mode 100644 index 0000000..a489214 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/CORS.kt @@ -0,0 +1,25 @@ +package ink.snowflake.server.plugins + +import ink.snowflake.server.utils.AppConfig +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.partialcontent.* +import io.ktor.server.plugins.autohead.AutoHeadResponse +import io.ktor.server.plugins.cors.routing.* + + +fun Application.configureCORS() { + install(CORS) { + anyHost() + allowHost("localhost:8089") + allowHost("127.0.0.1:8089") + allowHost("171.212.101.199:8089") + allowHost("debug.scaitcn.com:8089") + + // 进一步配置 CORS + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.Authorization) + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/Databases.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Databases.kt new file mode 100644 index 0000000..cc929a7 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Databases.kt @@ -0,0 +1,13 @@ +package ink.snowflake.server.plugins + +import ink.snowflake.server.utils.AppConfig +import org.jetbrains.exposed.sql.* + +fun configureDatabases(config: AppConfig) { + Database.connect( + url = config.dbUrl, + driver = config.dbDriver, + user =config.dbUser, + password = config.dbPassword, + ) +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/Security.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Security.kt new file mode 100644 index 0000000..fbf80aa --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Security.kt @@ -0,0 +1,40 @@ +package ink.snowflake.server.plugins + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import ink.snowflake.server.model.response.* +import ink.snowflake.server.utils.AppConfig +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.text.DateFormat +import java.util.* + +fun Application.configureSecurity(config: AppConfig) { + authentication { + jwt { + realm = config.jwtRealm + verifier( + JWT + .require(Algorithm.HMAC256(config.jwtSecret)) + .withAudience(config.jwtAudience) + .withIssuer(config.jwtDomain) + .build() + ) + validate { credential -> + if (credential.payload.audience.contains(config.jwtAudience) + && credential.payload.getClaim("token_type").asString() == "access_token" + ) { + JWTPrincipal(credential.payload) + } else { + null + } + } + } + } + +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/Serialization.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Serialization.kt new file mode 100644 index 0000000..54a1345 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Serialization.kt @@ -0,0 +1,20 @@ +package ink.snowflake.server.plugins + +import io.ktor.http.* +import io.ktor.serialization.* +import io.ktor.serialization.gson.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + gson{ + setPrettyPrinting() + } + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/StaticPath.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/StaticPath.kt new file mode 100644 index 0000000..726baeb --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/StaticPath.kt @@ -0,0 +1,16 @@ +package ink.snowflake.server.plugins + +import ink.snowflake.server.utils.AppConfig +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import io.ktor.server.plugins.autohead.AutoHeadResponse +import io.ktor.server.plugins.partialcontent.PartialContent +import io.ktor.server.routing.* +import org.jetbrains.exposed.sql.* +import java.io.File + +fun Application.configureStaticPath() { + install(PartialContent) + install(AutoHeadResponse) +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/StatusPages.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/StatusPages.kt new file mode 100644 index 0000000..65bf048 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/StatusPages.kt @@ -0,0 +1,87 @@ +package ink.snowflake.server.plugins + +import ink.snowflake.server.model.response.BaseResponse +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* + +fun Application.configureStatusPages() { + + install(StatusPages) { + // 处理特定的异常 + exception { call, cause -> + call.respondText("App in illegal state as ${cause.message}", status = HttpStatusCode.InternalServerError) + } + exception { call, cause -> + // 记录异常信息,方便后续分析 + println("Unhandled exception: ${cause.localizedMessage}" + cause) + call.respond(HttpStatusCode.InternalServerError, "Server Error") + } + exception { call, cause -> + call.respond( + HttpStatusCode.InternalServerError, BaseResponse( + status = false, + message = cause.localizedMessage + ) + ) + } + // 处理不同的 HTTP 状态码 + status(HttpStatusCode.Unauthorized) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Unauthorized," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.BadRequest) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Bad Request," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.Forbidden) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Forbidden," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.NotFound) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Not Found," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.MethodNotAllowed) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Method Not Allowed," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.Conflict) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Conflict," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.InternalServerError) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Internal Server Error," + status.description + ) + call.respond(status, response) + } + status(HttpStatusCode.ServiceUnavailable) { call, status -> + val response = BaseResponse( + status = false, + message = "${status.value} Service Unavailable," + status.description + ) + call.respond(status, response) + } + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/plugins/Templating.kt b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Templating.kt new file mode 100644 index 0000000..d42c20e --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/plugins/Templating.kt @@ -0,0 +1,21 @@ +package ink.snowflake.server.plugins + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.thymeleaf.Thymeleaf +import io.ktor.server.thymeleaf.ThymeleafContent +import kotlinx.css.* +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver + +fun Application.configureTemplating() { + install(Thymeleaf) { + setTemplateResolver(ClassLoaderTemplateResolver().apply { + prefix = "templates/thymeleaf/" + suffix = ".html" + characterEncoding = "utf-8" + }) + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/repository/ImageDataBase.kt b/ktor/src/main/kotlin/ink/snowflake/server/repository/ImageDataBase.kt new file mode 100644 index 0000000..b24a26e --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/repository/ImageDataBase.kt @@ -0,0 +1,32 @@ +package ink.snowflake.server.repository + +import ink.snowflake.server.model.database.ImageAnalyticsRequest +import ink.snowflake.server.model.database.VideoTable +import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio +import ink.snowflake.server.model.database.VideoTable.vACountPeople +import ink.snowflake.server.model.database.VideoTable.vAMaxAction +import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime +import ink.snowflake.server.model.database.VideoTable.vATotalPeople +import ink.snowflake.server.model.database.VideoTable.vStartDateTime +import ink.snowflake.server.model.response.Area +import ink.snowflake.server.model.response.DetailItem +import ink.snowflake.server.model.response.ItemStyle +import ink.snowflake.server.model.response.VideoAnalyticsData +import ink.snowflake.server.model.response.VideoAnalyticsDetail +import ink.snowflake.server.model.response.VideoListResponse +import ink.snowflake.server.utils.database.ImageDao +import ink.snowflake.server.utils.database.VideoDao +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import java.time.format.DateTimeFormatter + +class ImageDataBase { + + fun saveImageAnalyticsData(request: ImageAnalyticsRequest) { + return ImageDao.insertImageAnalyticsData(request) + } + + fun getImageList(): List { + return ImageDao.getVideoList() + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/repository/UserDataBase.kt b/ktor/src/main/kotlin/ink/snowflake/server/repository/UserDataBase.kt new file mode 100644 index 0000000..b12ea92 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/repository/UserDataBase.kt @@ -0,0 +1,49 @@ +package ink.snowflake.server.repository + +import ink.snowflake.server.utils.database.UserDAO +import java.security.MessageDigest +import kotlin.text.Charsets.UTF_8 + +class UserDataBase { + + fun login(email: String, password: String): Int { + // 查找用户 + val userId = UserDAO.getUserIdByEmail(email) + return if(userId == null){ + // 账号不存在 + -1 + }else{ + val userPassword = UserDAO.getPasswordById(userId) + // 验证密码 + if (password == userPassword) { + // 登录成功 + userId + } else { + // 账号密码不匹配 + -2 + } + } + } + + /** + * 正数:userId 负数:错误码 + */ + fun registerByEmail(email: String, password: String): Int { + // 查找用户 + val userId = UserDAO.getUserIdByEmail(email) + return if(userId != null){ + // 如果用户已存在,返回 0 + 0 + }else{ + // 用户不存在,插入新用户 + UserDAO.registerByEmailAndGetId(email, password) + } + } + + + fun hashPassword(password: String): String { + val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8)) + return bytes.joinToString("") { "%02x".format(it) } + } + +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/repository/VideoDataBase.kt b/ktor/src/main/kotlin/ink/snowflake/server/repository/VideoDataBase.kt new file mode 100644 index 0000000..902ba67 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/repository/VideoDataBase.kt @@ -0,0 +1,158 @@ +package ink.snowflake.server.repository + +import ink.snowflake.server.SERVER_PATH +import ink.snowflake.server.model.database.VideoTable +import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio +import ink.snowflake.server.model.database.VideoTable.vACountPeople +import ink.snowflake.server.model.database.VideoTable.vAMaxAction +import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime +import ink.snowflake.server.model.database.VideoTable.vATotalPeople +import ink.snowflake.server.model.database.VideoTable.vStartDateTime +import ink.snowflake.server.model.response.Area +import ink.snowflake.server.model.response.DetailItem +import ink.snowflake.server.model.response.ItemStyle +import ink.snowflake.server.model.response.VideoAnalyticsData +import ink.snowflake.server.model.response.VideoAnalyticsDetail +import ink.snowflake.server.model.request.VideoAnalyticsRequest +import ink.snowflake.server.model.response.VideoListResponse +import ink.snowflake.server.utils.database.VideoDao +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import java.time.format.DateTimeFormatter + +class VideoDataBase { + + fun saveVideoAnalyticsData(request: VideoAnalyticsRequest) { + return VideoDao.insertVideoAnalyticsData(request) + } + + fun getVideoList(): List { + return VideoDao.getVideoList() + } + + fun getAnalyticsDetailByVideoId(vId: Int): VideoAnalyticsDetail? { + // 查询视频信息 + val video = VideoDao.getAnalyticsDetailByVideoId(vId) + if (video == null) { + return null + } + // 查询相关的分析详情 + val details = VideoDao.selectVideoDetailByVid(vId) + + // 用于返回的数据 + val yTotalData = mutableListOf>() // (时间, 总人数) + val yMaskedData = mutableListOf>() // (时间, 佩戴口罩人数) + val areaData = mutableListOf>() + val detailList = mutableListOf() + // 颜色映射 + val colors = mapOf( + "feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色 + "disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色 + "other" to "rgba(255, 0, 0, 0.4)" // 淡红色 + ) + // 生成 yTotalData 和 yMaskedData,同时计算 areaData + var lastAction: String? = null + var areaStartTime: String? = null + var currentColor: String? = null + + for (detail in details) { + // 获取视频开始时间 + val vStartDatetime: LocalDateTime = video[vStartDateTime]!! + val timeStr = vStartDatetime.toJavaLocalDateTime() + .plusSeconds(detail.a_time_stamp.toLong()) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + // 添加总人数和口罩佩戴人数 + yTotalData.add(timeStr to detail.a_total_people) + yMaskedData.add(timeStr to detail.a_total_people_masked) + + // 处理 areaData,根据 a_action 判断是否需要创建新的区域 + if (detail.a_action != "--") { // 只处理非 "--" 动作 + if (lastAction == null || lastAction != detail.a_action) { + // 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域 + if (areaStartTime != null) { + areaData.add( + listOf( + Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), + Area(timeStr, ItemStyle(currentColor ?: "#FF0000")) + ) + ) + } + // 添加到 detailList,记录动作开始的时刻 + detailList.add( + DetailItem(getFriendlyActionName(detail.a_action), time = timeStr) + ) + + // 更新为新的动作 + lastAction = detail.a_action + areaStartTime = timeStr + currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色 + } + } else { + // 如果当前动作为 "--",则结束当前区域,并重置状态 + if (areaStartTime != null) { + // 添加到 detailList,记录动作开始的时刻 + detailList.add( + DetailItem(getFriendlyActionName(detail.a_action), timeStr) + ) + // 结束当前区域 + areaData.add( + listOf( + Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), + Area(timeStr, ItemStyle(currentColor ?: "#FF0000")) + ) + ) + // 重置状态 + lastAction = null + areaStartTime = null + currentColor = null + } + } + } + // 处理最后一个区域的结束时间 + if (areaStartTime != null && lastAction != null) { + areaData.add( + listOf( + Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), + Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000")) + ) + ) + } + val analyticsData = VideoAnalyticsData( + yTotalData = yTotalData, + yMaskedData = yMaskedData, + areaData = areaData + ) + // 返回数据 + return VideoAnalyticsDetail( + v_name = video[VideoTable.vName], + v_video_play_path = "http://${SERVER_PATH}:9000/video/" + video[VideoTable.vObjectName], + v_file_name = video[VideoTable.vFileName], + v_duration = video[VideoTable.vDuration], + v_size = video[VideoTable.vSize], + v_start_datetime = video[VideoTable.vStartDateTime]?.toString() ?: "", + v_video_codec = video[VideoTable.vVideoCodec], + v_audio_codec = video[VideoTable.vAudioCodec], + v_overall_bit_rate = video[VideoTable.vOverallBitRate], + v_resolution = video[VideoTable.vResolution], + v_a_time = video[VideoTable.vATime].toString(), + v_a_total_people = video[vATotalPeople], // 总人数 + v_a_count_people = video[vACountPeople], // 佩戴口罩人数 + v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间 + v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作 + v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例 + v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData + v_details_list = detailList + ) + } +} + +private fun getFriendlyActionName(name: String): String { + return if (name == "feed") { + "喂桑" + } else if (name == "disinfection") { + "消毒" + } else { + name + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt new file mode 100644 index 0000000..f57e9b6 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt @@ -0,0 +1,30 @@ +package ink.snowflake.server.route + +import com.google.gson.Gson +import ink.snowflake.server.model.response.BaseResponse +import ink.snowflake.server.model.response.DeviceItem +import ink.snowflake.server.utils.runCommand +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.io.BufferedReader +import java.io.InputStreamReader +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.* +import java.io.File + +fun Application.mainFunc() { + routing { + get("/") { +// call.respondFile(File("src/main/resources/page/html/login.html")) +// call.respondRedirect("html/login.html") + } + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/configureSockets.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/configureSockets.kt new file mode 100644 index 0000000..b4cc2f1 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/configureSockets.kt @@ -0,0 +1,21 @@ +package ink.snowflake.server.route + +import io.ktor.serialization.gson.* +import io.ktor.server.application.* +import io.ktor.server.websocket.* +import io.ktor.server.websocket.WebSockets +import kotlin.time.Duration.Companion.seconds + +fun Application.configureSockets() { + install(WebSockets) { + contentConverter = GsonWebsocketContentConverter() + pingPeriod = 15.seconds + timeout = 15.seconds + masking = false + } +// install(WebSockets) { +// pingPeriod = Duration.ofSeconds(15) +// timeout = Duration.ofSeconds(15) +// maxFrameSize = Long.MAX_VALUE +// } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/Chat.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/Chat.kt new file mode 100644 index 0000000..540c302 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/Chat.kt @@ -0,0 +1,144 @@ +package ink.snowflake.server.route.func + +import com.cyberecho.mdoel.database.WSChatRecords +import ink.snowflake.server.gson +import ink.snowflake.server.model.ai.ChatRequest +import ink.snowflake.server.model.ai.ChatResponse +import ink.snowflake.server.model.response.BaseResponse +import ink.snowflake.server.utils.WebSocketManager +import ink.snowflake.server.utils.database.ChatRecordsDao +import ink.snowflake.server.utils.getUserIdByToken +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +fun Application.chat() { + val messageResponseFlow = MutableSharedFlow() + val sharedFlow = messageResponseFlow.asSharedFlow() + + val client = HttpClient(CIO) { + } + routing { + route("/ws") { + webSocket("/ws") { + send("You are connected to WebSocket!") + val job = launch { + sharedFlow.collect { message -> + send(message) + } + } + runCatching { + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + val receivedText = frame.readText() + messageResponseFlow.emit("回复$receivedText") + println("回复$receivedText") + } + } + }.onFailure { exception -> + println("WebSocket exception: ${exception.localizedMessage}") + }.also { + job.cancel() + } + } + get("/getAliveUserSize") { + call.respond(BaseResponse(data = "当前活跃用户数:" + WebSocketManager.getAllConnectionsSize())) + } + authenticate { + webSocket("/echo") { + val userId = getUserIdByToken(call) + if (userId == null) { + // 如果 token 无效,关闭连接 + close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Invalid token")) + return@webSocket + } + // 连接成功 + WebSocketManager.addConnection(userId, this) + try { + // 监听并接收来自客户端的消息 + incoming.consumeEach { frame -> + when (frame) { + is Frame.Text -> { + // 处理文本消息 + val text = frame.readText() + val userMessage = gson.fromJson(text, WSChatRecords::class.java) + println("收到消息: $userMessage") + launch { + ChatRecordsDao.insertChatRecord( + userId = userId, + message = userMessage.content, + messageType = userMessage.messageType, + sendAt = userMessage.timestamp, + from = userMessage.msgFrom, + emotion = "normal" + ) + val memory = ChatRecordsDao.getRecentChatRecords(userId) + val memoryMsg = memory.map { ChatRequest.Message(it.message,it.from) }.toList().reversed() + val result = client.post("http://localhost:11434/api/chat") { + contentType(ContentType.Application.Json) + setBody(gson.toJson(ChatRequest(memoryMsg))) + } +// val result2 = client.post("http://localhost:11434/api/chat") { +// contentType(ContentType.Application.Json) +// setBody(gson.toJson(ChatRequest(listOf(ChatRequest.Message(userMessage.content))))) +// } + val res = gson.fromJson(result.bodyAsText(), ChatResponse::class.java) + println(result.bodyAsText()) + val wsChatRecords = WSChatRecords( + aiId = userMessage.aiId,// 交流的对象AI_ID 从哪儿来回哪儿去 + messageType = "text", + content = res.message.content, + timestamp = System.currentTimeMillis(), + msgFrom = "AI" + ) + WebSocketManager.sendMessageToUser(userId, wsChatRecords) + ChatRecordsDao.insertChatRecord( + userId = wsChatRecords.aiId, + message = wsChatRecords.content, + messageType = wsChatRecords.messageType, + sendAt = wsChatRecords.timestamp, + from = wsChatRecords.msgFrom, + emotion = "normal" + ) + } + } + // 处理二进制消息 + is Frame.Binary -> { + val data = frame.readBytes() + } + // 处理 pong 帧 + is Frame.Pong -> {} + // 处理关闭帧 + is Frame.Close -> { + WebSocketManager.removeConnection(userId) + close() + return@consumeEach + } + + else -> Unit + } + } + } catch (e: Exception) { + // 错误处理 + println("WebSocket error: ${e.localizedMessage}") + } finally { + println("User $userId disconnected") + WebSocketManager.removeConnection(userId) + } + } + } + } + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/ImageAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/ImageAnalytics.kt new file mode 100644 index 0000000..fe04262 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/ImageAnalytics.kt @@ -0,0 +1,42 @@ +package ink.snowflake.server.route.func + +import ink.snowflake.server.model.database.ImageAnalyticsRequest +import ink.snowflake.server.model.request.VideoAnalyticsRequest +import ink.snowflake.server.model.response.* +import ink.snowflake.server.repository.ImageDataBase +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.websocket.* + +fun Application.ImageAnalytics() { + val repository = ImageDataBase() + routing { + route("/api") { + // 上传分析结果 + post("/saveImageAnalyticsData") { + val request = call.receive() + call.respond(BaseResponse(data = repository.saveImageAnalyticsData(request))) + } + authenticate { + // 拍照保存为图片 并且调用Python程序进行分析 + get("/takePhoto") { + val camera = call.request.queryParameters["cameraId"] + if (camera.isNullOrEmpty()) { + call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null)) + return@get + } + stopHLSStream(camera) + call.respond(BaseResponse(message = "摄像头流已停止", data = null)) + } + // 获取已分析视频列表 + get("/getImageList") { + val res = repository.getImageList() + call.respond(BaseResponse(data = res)) + } + } + } + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/RemoteDebug.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/RemoteDebug.kt new file mode 100644 index 0000000..f7f4b30 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/RemoteDebug.kt @@ -0,0 +1,165 @@ +package ink.snowflake.server.route.func + +import com.google.gson.Gson +import ink.snowflake.server.SERVER_PATH +import ink.snowflake.server.model.request.DevicesInfo +import ink.snowflake.server.model.response.* +import ink.snowflake.server.utils.runCommand +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.auth.Auth +import io.ktor.client.plugins.auth.providers.BasicAuthCredentials +import io.ktor.client.plugins.auth.providers.basic +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.auth.authenticate +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.websocket.* +import io.ktor.utils.io.* +import io.ktor.websocket.* +import io.ktor.websocket.send +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.launch +import java.io.BufferedReader +import java.io.InputStreamReader +//import sun.jvm.hotspot.HelloWorld.e +import java.util.* + +fun Application.RemoteDebug() { + val adbClients = Collections.synchronizedList(ArrayList()) // 线程安全的客户端列表 + val client = HttpClient(CIO) { + install(Auth) { + basic { + credentials { + 1 + BasicAuthCredentials(username = "roto", password = "jjkj@2021.cn") + } + } + } + } + routing { + route("/api") { + authenticate { + route("/remote") { + get("/connect") { + val ip = call.parameters["ip"] + val port = call.parameters["port"] + if (ip != null && port != null) { + val result = runAdbCommand("connect $ip:$port") + call.respond(BaseResponse(data = result)) + } else { + call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null)) + } + } + get("/connectLocalDevice") { + val port = call.parameters["port"] + if (port != null) { + val result = runAdbCommand("connect ${SERVER_PATH}:$port") + call.respond(BaseResponse(data = result)) + } else { + call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null)) + } + } + get("/disConnectAll") { + val result = runAdbCommand("disconnect") + call.respond(BaseResponse(data = result)) + } + get("/runLinuxCommand") { + val command = call.parameters["command"] + if (command != null) { + call.respond(BaseResponse(data = runCommand(command))) + } else { + call.respond(BaseResponse(status = false, message = "Linux命令不可为空", data = null)) + } + } + get("/refreshDeviceList") { + try { + val name = call.parameters["name"] + val response: HttpResponse = client.get("http://171.212.101.201:65534/api/proxy/tcp") + val responseBody: String = response.bodyAsText() + val devicesInfo: DevicesInfo = Gson().fromJson(responseBody, DevicesInfo::class.java) + val onlineDevices = devicesInfo.proxies.stream() + .filter { it.status == "online" && it.conf != null && it.conf.remotePort >= 10000 && it.conf.remotePort <= 20000 } + val devices: MutableList = mutableListOf() + for (data in onlineDevices) { + if (name == "null" || name == null || data.name.contains(name) && data.conf != null) { + devices.add(DeviceItem(data.name, data.conf!!.remotePort)) + } + } + call.respond(BaseResponse(data = devices)) + } catch (e: Exception) { + call.respond( + BaseResponse( + status = false, + message = "端口信息请求失败:${e.message}", + data = null + ) + ) + } + } + } + } + } + webSocket("/logStream") { + send("日志系统连接成功") + + var process: Process? = null // 保存 Process 对象,便于后续关闭 + var filter = "" + + try { + // 启动 adb logcat 或其他命令,并通过 WebSocket 发送日志 + process = Runtime.getRuntime().exec("adb logcat")//ping 127.0.0.1 测试用 + val reader = BufferedReader(InputStreamReader(process.inputStream)) + + // 异步读取日志并通过 WebSocket 推送 + launch { + var line: String? + while (reader.readLine().also { line = it } != null) { + if (filter.isNotEmpty()) { + if (line != null && line.lowercase().contains(filter.lowercase())) { + send(line) + } + } else { + send(line ?: "") + } + } + } + + // 持续监听 WebSocket 消息 + incoming.consumeEach { frame -> + when (frame) { + is Frame.Close -> { + // 客户端断开连接时,终止命令 + process?.destroy() // 终止命令 + clients.remove(this) // 移除客户端 + return@consumeEach + } + + is Frame.Text -> { + filter = frame.readText() + } + // 处理其他帧类型 + is Frame.Binary -> TODO() + is Frame.Ping -> TODO() + is Frame.Pong -> TODO() + } + } + } catch (e: Exception) { + // 处理异常情况 + clients.remove(this) // 移除客户端 + } finally { + // 确保在连接关闭时终止后台进程并移除客户端 + process?.destroy() // 终止命令 + clients.remove(this) + } + } + + } +} + +fun runAdbCommand(command: String): String = runCommand("adb $command") diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/User.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/User.kt new file mode 100644 index 0000000..0c9a581 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/User.kt @@ -0,0 +1,256 @@ +package ink.snowflake.server.route.func + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import ink.snowflake.server.utils.database.AIDao +import ink.snowflake.server.model.request.CommonRequest +import ink.snowflake.server.model.request.LoginRequest +import ink.snowflake.server.model.request.RefreshTokenRequest +import ink.snowflake.server.model.request.RegisterRequest +import ink.snowflake.server.model.response.* +import ink.snowflake.server.repository.UserDataBase +import ink.snowflake.server.utils.AppConfig +import ink.snowflake.server.utils.database.UserDAO +import ink.snowflake.server.utils.getUserIdByToken +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.coroutines.launch +import org.redisson.Redisson +import org.redisson.api.RBucket +import org.redisson.api.RedissonClient +import org.redisson.config.Config +import java.text.DateFormat +import java.time.Duration +import java.util.* +import javax.mail.* +import javax.mail.internet.InternetAddress +import javax.mail.internet.MimeMessage + +// 配置和初始化 Redis 客户端 +fun setupRedis(): RedissonClient { + val config = Config() + config.useSingleServer().setAddress("redis://localhost:6379") + return Redisson.create(config) +} + +fun Application.User(config: AppConfig) { + val repository = UserDataBase() + // 初始化 Redis 连接 + val redisClient: RedissonClient = setupRedis() + routing { + route("/api") { + route("/user") { + post("/login") { + val loginRequest = call.receive() + val email = loginRequest.account + val password = loginRequest.password + val userId = repository.login(email, password) + call.respond( + if (userId == -1) { + BaseResponse(status = false, message = "尚未注册", data = null) + } else if (userId == -2) { + BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null) + } else { + BaseResponse( + status = true, data = LoginResponse( + userId.toString(), + generateAccessToken(config, userId), + generateRefreshToken(config, userId) + ) + ) + // match == -1 + } + ) + } + post("/sendCode") { + val account = call.receive() + // 将验证码存入 Redis,设置过期时间为 10 分钟 + val bucket: RBucket = redisClient.getBucket("verificationCode:${account.str}") + // 生成随机验证码(6位数字) + val verificationCode = generateVerificationCode() + println("验证码:$verificationCode") + // 10分钟的过期时间 + bucket.set(verificationCode, Duration.ofMinutes(10)) + launch { + sendVerificationEmail(config, account.str, verificationCode) + } + call.respond(BaseResponse(status = true, message = "已发送验证码", data = null)) + } + post("/register") { + // 可能返回的情况:1. 注册成功 -1. 账户已存在 -2. 验证码错误 -3. 验证码过期 + val register = call.receive() + val account = register.account + // 从 Redis 获取保存的验证码 + val bucket: RBucket = redisClient.getBucket("verificationCode:$account") + val storedCode = bucket.get() + if (storedCode == null) { + call.respond(BaseResponse(status = false, message = "验证码过期", data = null)) + } else if (storedCode != register.code) { + call.respond(BaseResponse(status = false, message = "验证码错误", data = null)) + } else { + val userId = repository.registerByEmail(register.account, register.password) + if (userId > 0) { + call.respond( + BaseResponse( + status = true, message = "注册成功", data = + BaseResponse( + status = true, data = LoginResponse( + account, + generateAccessToken(config, userId), + generateRefreshToken(config, userId) + ) + ) + ) + ) + } else { + call.respond(BaseResponse(status = false, message = "账户已存在", data = null)) + } + } + } + post("/refreshToken") { + try { + val token = call.receive() + val verifier = JWT.require(Algorithm.HMAC256(config.jwtSecret)) + .withAudience(config.jwtAudience) + .withIssuer(config.jwtDomain) + .build() + val decodedJWT = verifier.verify(token.refreshToken) + val tokenType = decodedJWT.getClaim("token_type").asString() + if (tokenType != "refresh_token") { + call.respond( + BaseResponse( + status = false, + message = "拿什么乱七八糟的东西跟我换Access Token呢,???", + data = null + ) + ) + } + val userId = decodedJWT.getClaim("user_id").asInt() + // 生成新的access token和refresh token + call.respond( + BaseResponse( + status = true, data = RefreshTokenResponse( + generateAccessToken(config, userId), + generateRefreshToken(config, userId) + ) + ) + ) + call.respond(BaseResponse(data = generateAccessToken(config, userId))) + } catch (ex: Exception) { + call.respond(BaseResponse(status = false, message = "token解析错误", data = null)) + } + } + authenticate { + get("/getAiList") { +// val allAIProfile = AIDao.getAllAIProfiles().stream().map { +// AiListForUser(aiId = it.id, aiName = it.name, aiAvatar = it.avatarUrl ?: "") +// }.toList() +// call.respond(BaseResponse(data = allAIProfile)) + } + get("/getUserInfo") { + val userId = getUserIdByToken(call) + if (userId != null) { + val userInfo = UserDAO.getUserInfoByUserId(userId) + if (userInfo != null) { + val response = BaseResponse(data = userInfo) + call.respond(response) + } else { + call.respond(BaseResponse(data = "查无此人")) + } + } else { + call.respond(BaseResponse(data = "Token出错")) + } + } + } + } + } + } +} + +fun generateAccessToken(config: AppConfig, userId: Int): Token { +// return generateToken(config, userId, 120, "access_token") + return generateToken(config, userId, 2 * 60 * 60, "access_token") +} + +fun generateRefreshToken(config: AppConfig, userId: Int): Token { + return generateToken(config, userId, 10 * 24 * 60 * 60, "refresh_token") +} + + +fun generateToken(config: AppConfig, userId: Int, second: Int, tokenType: String): Token { + val expiresAt = Date(System.currentTimeMillis() + second * 1000) // + val token = JWT.create() + .withAudience(config.jwtAudience) + .withIssuer(config.jwtDomain) + .withClaim("user_id", userId) + .withClaim("token_type", tokenType) + .withExpiresAt(expiresAt) + .sign(Algorithm.HMAC256(config.jwtSecret)) + + return Token(token, expiresAt.time, DateFormat.getDateTimeInstance().format(expiresAt)) +} + +// 生成4位随机验证码 +fun generateVerificationCode(): String { + return String.format("%04d", Random().nextInt(10000)) +} + +fun sendVerificationEmail(config: AppConfig, recipientEmail: String, verificationCode: String) { + // 设置邮件会话的属性 + val properties = Properties().apply { + put("mail.smtp.host", config.smtpHost) + put("mail.smtp.port", config.smtpPort) + put("mail.smtp.auth", "true") // 启用身份验证 + put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory") // 启用 SSL + put("mail.smtp.socketFactory.fallback", "false") // 禁用备用连接 + } + // 创建会话 + val session = Session.getInstance(properties, object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication(config.smtpUser, config.smtpPassword) + } + }) + try { + // Create email content with a more polished template + val message = MimeMessage(session).apply { + setFrom(InternetAddress(config.smtpUser)) + setRecipient(Message.RecipientType.TO, InternetAddress(recipientEmail)) + subject = "Welcome to CyberEcho! Verification Code:$verificationCode" + val htmlContent = """ + + +
+

Welcome to CyberEcho!

+

Thank you for signing up with CyberEcho! We are excited to have you on board.

+ +

To complete your registration, please use the verification code below:

+ +
+ $verificationCode +
+ +

This code is valid for the next 10 minutes. If you did not request this, please ignore this email.

+ +

Please do not reply to this email. This inbox is not monitored.

+
+

+ Best regards
+ The CyberEcho Account Team +

+
+ + + """ + setContent(htmlContent, "text/html") + } + // 发送邮件 + Transport.send(message) + println("Verification email sent successfully to $recipientEmail") + } catch (e: MessagingException) { + e.printStackTrace() + println("Failed to send verification email.") + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalytics.kt new file mode 100644 index 0000000..bb0b620 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalytics.kt @@ -0,0 +1,159 @@ +package ink.snowflake.server.route.func + +import ink.snowflake.server.VIDEO_INPUT_PATH +import ink.snowflake.server.model.request.VideoAnalyticsRequest +import ink.snowflake.server.model.response.* +import ink.snowflake.server.repository.VideoDataBase +import ink.snowflake.server.utils.WebSocketManager.broadcastMessage +import ink.snowflake.server.utils.runCommand +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.consumeEach +import java.io.File +import java.io.IOException +import java.util.* + +val clients = Collections.synchronizedList(ArrayList()) // 线程安全的客户端列表 +var aiState = "等待分析任务中" + +fun Application.VideoAnalytics() { + val repository = VideoDataBase() + routing { + // 实时发送AI状态 + webSocket("/handleState") { // WebSocket 路由 + clients.add(this) // 添加当前连接的客户端 + send(aiState) // 向客户端发送连接成功消息 + try { + incoming.consumeEach { frame -> // 持续接收消息 + when (frame) { + is Frame.Text -> { + aiState = frame.readText() // 更新状态 + broadcastMessage(aiState) // 使用封装的方法广播消息 + } + + is Frame.Close -> { + println("Closed") + clients.remove(this) + close() // 确保关闭 WebSocket 连接 + return@consumeEach + } + // 其他消息类型的处理 + is Frame.Binary -> TODO() // 处理二进制消息 + is Frame.Ping -> TODO() // 处理 Ping 消息 + is Frame.Pong -> TODO() // 处理 Pong 消息 + } + } + } catch (e: Exception) { + // 处理接收消息时的异常 + close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected")) + e.printStackTrace() + } finally { + clients.remove(this) // 确保在连接关闭时移除客户端 + } + } + route("/api") { + // 上传分析结果 + post("/saveVideoAnalyticsData") { + val request = call.receive() + // todo 上传这里未做测试 + call.respond(BaseResponse(data = repository.saveVideoAnalyticsData(request))) + } + authenticate { + post("/upload") { + val multipart = call.receiveMultipart() //1G + // 确保 uploads 目录存在 + val uploadDir = File(VIDEO_INPUT_PATH) + if (!uploadDir.exists()) { + if (!uploadDir.mkdirs()) { + println("无法创建目录") + throw IOException("Failed to create upload directory.") + } + } + var fileName = "" + var name = "" + var datetime = "" + broadcastMessage("正在上传数据") + multipart.forEachPart { part -> + when (part) { + is PartData.FileItem -> { + fileName = part.originalFileName ?: "unknown" + val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 + //ktor3 +// file.outputStream().use { outputStream -> +// val writableChannel = Channels.newChannel(outputStream) +// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel +// } + //ktor2 + part.streamProvider().use { inputStream -> + file.outputStream().buffered().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + + is PartData.FormItem -> { + when (part.name) { + "name" -> name = part.value + "datetime" -> datetime = part.value + } + } + + else -> part.dispose() + } + } + call.respond(BaseResponse(message = "上传成功", data = null)) + broadcastMessage("上传完成,开始启动AI引擎") + val command = listOf( + "/usr/bin/python3", + "/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py", + "$VIDEO_INPUT_PATH$fileName", + datetime, + name + ) + println("-----------------" + command.joinToString(" ")) + runCommand(command) { + println(it) + } + } + // 获取已分析视频列表 + get("/getVideoList") { + val res = repository.getVideoList() + call.respond(BaseResponse(data = res)) + } + // 获取某视频分析详情 + get("/getAnalyticsDetailByVideoId") { + // 获取 vId 参数 + val vIdParam = call.parameters["vId"] + val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全 + if (vId == null) { + call.respond(BaseResponse(status = true, message = "Invalid vId", data = null)) + return@get + } + val result = repository.getAnalyticsDetailByVideoId(vId) + if (result != null) { + call.respond(BaseResponse(data = result)) + } + } + } + } + } +} + + +suspend fun broadcastMessage(message: String) { // 封装的广播消息方法 + println("发送消息:$message") + clients.forEach { client -> + try { + client.send(message) // 发送消息到每个客户端 + } catch (e: Exception) { + println("发送消息给客户端时出错,移除客户端: ${e.message}") + clients.remove(client) // 移除已断开的客户端 + } + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalyticsJetson.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalyticsJetson.kt new file mode 100644 index 0000000..bb45d84 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/func/VideoAnalyticsJetson.kt @@ -0,0 +1,187 @@ +package ink.snowflake.server.route.func + +import ink.snowflake.server.model.response.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.content.files +import io.ktor.server.http.content.static +import io.ktor.server.routing.* +import io.ktor.server.response.* +import io.ktor.server.websocket.* +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +// 存储 FFmpeg 进程,key 是摄像头名称 +private val ffmpegProcesses = ConcurrentHashMap() + +fun Application.VideoAnalyticsJetson() { + + val streamFile = "camera/stream/" + + routing { + // 实时发送 AI 状态 + webSocket("/handleState1") { + } + + route("/api") { + static("/camera/stream") { +// files("camera/stream") // 确保 FFmpeg 输出的 HLS 片段和 m3u8 文件存放在这里 + files(File(streamFile)) + } + authenticate { + // 获取摄像头列表 + get("/getCameraDevices") { + call.respond(BaseResponse(data = getCameraDevices())) + } + + // 启动 HLS 流 + get("/startCamera") { + val camera = call.request.queryParameters["cameraId"] + if (camera.isNullOrEmpty()) { + call.respond(BaseResponse(status = false, message = "摄像头连接失败,摄像头名称错误", data = null)) + return@get + } + try { + val camAddr = streamFile + "output.m3u8" +// val camAddr = "C:\\Users\\Duan\\Desktop\\camera\\stream\\output.m3u8" + startHLSStream(camera, camAddr) + call.respond(BaseResponse(message = "摄像头连接成功", data = camAddr)) + } catch (e: Exception) { + call.respond(BaseResponse(status = false, message = "摄像头连接失败,${e.message}", data = null)) + } + } + + // 关闭摄像头流 + get("/stopCamera") { + val camera = call.request.queryParameters["cameraId"] + if (camera.isNullOrEmpty()) { + call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null)) + return@get + } + stopHLSStream(camera) + call.respond(BaseResponse(message = "摄像头流已停止", data = null)) + } + } + } + } + + // 确保应用退出时清理所有 FFmpeg 进程 + Runtime.getRuntime().addShutdownHook(Thread { + ffmpegProcesses.forEach { (camera, process) -> + println("应用退出,关闭摄像头流: $camera") + process.destroy() + } + }) +} + +// 获取可用摄像头设备列表 +fun getCameraDevices(): List { + val command = if (System.getProperty("os.name").contains("Windows")) { + arrayOf("ffmpeg", "-f", "dshow", "-list_devices", "true", "-i", "dummy") + } else { + arrayOf("v4l2-ctl", "--list-devices") + } + + val process = ProcessBuilder(*command) + .redirectErrorStream(true) // 合并标准输出和错误输出 + .start() + + val output = BufferedReader(InputStreamReader(process.inputStream)).readText() + println("命令输出:\n$output") + + val devices = mutableListOf() + val regex = if (System.getProperty("os.name").contains("Windows")) { + """"(.*?)"""".toRegex() + } else { + """\s+(.+/video\d+)""".toRegex() + } + + regex.findAll(output).forEach { + devices.add(it.groupValues[1]) + } + + return devices +} + +// 启动 HLS 流 +fun startHLSStream(cameraName: String, streamPath: String) { + if (ffmpegProcesses.containsKey(cameraName)) { + println("摄像头 $cameraName 已经在运行") + return + } + + val command: Array = if (System.getProperty("os.name").contains("Windows")) { + arrayOf( + "ffmpeg", "-f", "dshow", "-i", "video=$cameraName", + "-c:v", "libx264", "-preset", "fast", "-tune", "zerolatency", + "-f", "hls", "-hls_time", "3", + "-hls_list_size", "2", + "-hls_flags", "delete_segments", + streamPath + ) + } else { + arrayOf( + "ffmpeg", "-f", "v4l2", "-i", cameraName, + "-c:v", "libx264", "-preset", "fast", "-tune", "zerolatency", + "-f", "hls", "-hls_time", "3", + "-hls_list_size", "2", + "-hls_flags", "delete_segments", + streamPath + ) + } + + println(command.joinToString(" ")) + + try { + val process = ProcessBuilder(*command).start() + ffmpegProcesses[cameraName] = process + + Thread { + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + println(line) + } + } + }.start() + + Thread { + BufferedReader(InputStreamReader(process.errorStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + System.err.println(line) + } + } + }.start() + + } catch (e: Exception) { + e.printStackTrace() + } +} + +// 停止 HLS 流 +fun stopHLSStream(cameraName: String) { + val process = ffmpegProcesses.remove(cameraName) + if (process != null) { + println("正在关闭摄像头流: $cameraName") + process.destroy() + + try { + // 等待最多 3 秒,看看进程是否能正常退出 + if (!process.waitFor(3, TimeUnit.SECONDS)) { + println("摄像头流 $cameraName 进程未正常退出,尝试强制关闭") + process.destroyForcibly() // 强制关闭 + } + } catch (e: InterruptedException) { + println("关闭摄像头流时发生错误: ${e.message}") + process.destroyForcibly() + } + + println("摄像头流 $cameraName 已停止") + } else { + println("未找到正在运行的摄像头流: $cameraName") + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/AppConfig.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/AppConfig.kt new file mode 100644 index 0000000..a67b33a --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/AppConfig.kt @@ -0,0 +1,19 @@ +package ink.snowflake.server.utils +import io.ktor.server.config.* + + +class AppConfig(config: ApplicationConfig) { + val jwtAudience: String = config.property("ktor.security.jwt.audience").getString() + val jwtDomain: String = config.property("ktor.security.jwt.domain").getString() + val jwtRealm: String = config.property("ktor.security.jwt.realm").getString() + val jwtSecret: String = config.property("ktor.security.jwt.secret").getString() + val dbUrl: String = config.property("ktor.database.url").getString() + val dbDriver: String = config.property("ktor.database.driver").getString() + val dbUser: String = config.property("ktor.database.user").getString() + val dbPassword: String = config.property("ktor.database.password").getString() + + val smtpHost: String = config.property("ktor.mail.smtp.host").getString() + val smtpPort: Int = config.property("ktor.mail.smtp.port").getString().toInt() + val smtpUser: String = config.property("ktor.mail.smtp.user").getString() + val smtpPassword: String = config.property("ktor.mail.smtp.password").getString() +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/MyUtils.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/MyUtils.kt new file mode 100644 index 0000000..5205302 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/MyUtils.kt @@ -0,0 +1,90 @@ +package ink.snowflake.server.utils + +import ink.snowflake.server.model.response.UserInfo +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.format +import kotlinx.datetime.toJavaLocalDateTime +import java.io.BufferedReader +import java.io.InputStreamReader +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter +import java.util.* + +fun getUserIdByToken(call: ApplicationCall) : Int?{ + // 通过token获取user_id + return call.principal()?.payload?.getClaim("user_id")?.asInt() + +} + +fun formatDateToTargetString(date: Date, targetFormat: String): String { + val formatter = SimpleDateFormat(targetFormat) // 创建格式化器 + return formatter.format(date) // 格式化日期并返回字符串 +} + +fun formatLocalDateTimeToString(localDateTime: LocalDateTime): String { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // 创建格式化器 + return localDateTime.toJavaLocalDateTime().format(formatter) // 转换并格式化 +} + +fun runCommand(command: String): String { + println("command--$command") + return try { + val process = Runtime.getRuntime().exec(command) + // 等待命令执行完毕 + process.waitFor() + // 读取输出流 + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val errorReader = BufferedReader(InputStreamReader(process.errorStream)) + val output = StringBuilder() + // 读取标准输出 + reader.use { r -> + var line: String? = r.readLine() + while (line != null) { + output.append(line).append("\n") + line = r.readLine() + } + } + // 读取错误输出 + errorReader.use { er -> + var errorLine: String? = er.readLine() + while (errorLine != null) { + output.append("ERROR: ").append(errorLine).append("\n") + errorLine = er.readLine() + } + } + output.toString() + } catch (e: Exception) { + "Error running command: ${e.message}" + } +} + +fun runCommand(command: List, logCallback: (String) -> Unit): Process { + // 使用 ProcessBuilder 构建命令并启动 + val process = ProcessBuilder(command) + .redirectErrorStream(true) // 合并标准输出和错误输出 + .start() + + // 创建一个 CoroutineScope 来管理协程 + val scope = CoroutineScope(Dispatchers.IO) + + // 使用协程读取标准输出和错误输出 + scope.launch { + try { + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var line: String? + while (reader.readLine().also { line = it } != null) { + logCallback(line ?: "") // 将日志推送到回调 + } + } catch (e: Exception) { + logCallback("Error reading output: ${e.message}") + } + } + + return process +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/WebSocketManager.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/WebSocketManager.kt new file mode 100644 index 0000000..78f9bd7 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/WebSocketManager.kt @@ -0,0 +1,55 @@ +package ink.snowflake.server.utils + +import com.cyberecho.mdoel.database.WSChatRecords +import com.google.gson.Gson +import ink.snowflake.server.gson +import io.ktor.websocket.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap + +object WebSocketManager { + // 创建一个 CoroutineScope 来管理协程 + val scope = CoroutineScope(Dispatchers.IO) + + // 存储所有 WebSocket 连接的 Map,key 为 user_id,value 为 WebSocketSession + private val connections = ConcurrentHashMap() + + + // 添加 WebSocket 连接 + fun addConnection(userId: Int, session: WebSocketSession) { + connections[userId] = session + println("新连接:$userId") + } + + // 移除 WebSocket 连接 + fun removeConnection(userId: Int) { + connections.remove(userId) + println("断开连接:$userId") + } + + // 广播消息给所有连接的用户 + fun broadcastMessage(message: String) { + connections.forEach { userId, session -> + scope.launch { + session.send(message) + } + } + } + + // 向特定用户发送消息 + suspend fun sendMessageToUser(userId: Int, message: WSChatRecords) { + connections[userId]?.send(gson.toJson(message)) + } + + // 获取所有当前活跃的连接 + fun getAllConnections(): Set { + return connections.values.toSet() // 返回连接值的集合 + } + + // 获取所有当前活跃的连接 + fun getAllConnectionsSize(): Int { + return connections.size + } +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/database/AIDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/AIDao.kt new file mode 100644 index 0000000..c3b5d41 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/AIDao.kt @@ -0,0 +1,19 @@ +package ink.snowflake.server.utils.database + +import ink.snowflake.server.model.database.AIProfile +import ink.snowflake.server.model.database.AIProfilesTable +import ink.snowflake.server.model.database.toAIProfile +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction + +object AIDao { + /** + * 获取所有AI + */ + fun getAllAIProfiles(): List { + return transaction { + AIProfilesTable.selectAll().map { it.toAIProfile() } + } + } + +} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ChatRecordsDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ChatRecordsDao.kt new file mode 100644 index 0000000..9f0f6d3 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ChatRecordsDao.kt @@ -0,0 +1,49 @@ +package ink.snowflake.server.utils.database + +import ink.snowflake.server.model.database.* +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction + +object ChatRecordsDao { + /** + * 插入聊天记录 + */ + fun insertChatRecord( + userId: Int, + message: String, + messageType: String, + sendAt: Long, + from: String, + emotion: String? + ): Boolean { + return transaction { + ChatRecordsTable + .insertAndGetId { + it[ChatRecordsTable.userId] = userId + it[ChatRecordsTable.message] = message + it[ChatRecordsTable.messageType] = messageType + it[ChatRecordsTable.sentAt] = timestampLiteral(Clock.System.now()) + it[ChatRecordsTable.from] = from + it[ChatRecordsTable.emotion] = emotion + }.value > 0 + } + } + + /** + * 取最近五条聊天记录 + */ + fun getRecentChatRecords(userId: Int): List { + return transaction { + ChatRecordsTable + .selectAll() + .where { ChatRecordsTable.userId eq userId } + .orderBy(ChatRecordsTable.sentAt to SortOrder.DESC) + .limit(5) + .toList().map { it.toChatRecord() } + } + } +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ImageDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ImageDao.kt new file mode 100644 index 0000000..890eaae --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/ImageDao.kt @@ -0,0 +1,60 @@ +package ink.snowflake.server.utils.database + +import ink.snowflake.server.SERVER_PATH +import ink.snowflake.server.model.database.ImageAnalyticsRequest +import ink.snowflake.server.model.database.ImageTable +import ink.snowflake.server.model.response.VideoListResponse +import ink.snowflake.server.utils.formatLocalDateTimeToString +import kotlinx.datetime.toKotlinLocalDateTime +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Timestamp + +object ImageDao { + + fun insertImageAnalyticsData(request: ImageAnalyticsRequest) { + return transaction { + ImageTable.insert { + it[object_name] = request.object_name + it[upload_datetime] = Timestamp.valueOf(request.upload_datetime) + .toLocalDateTime().toKotlinLocalDateTime() + it[file_name] = request.file_name + it[resolution] = request.resolution + it[size] = request.size + it[cocoon_count] = request.cocoon_count + it[max_confidence] = request.max_confidence + it[min_confidence] = request.min_confidence + it[average_confidence] = request.average_confidence + it[other_info] = request.other_info // 直接存储 JSON + it[processing_time] = Timestamp.valueOf(request.processing_time) + .toLocalDateTime().toKotlinLocalDateTime() + } + } + } + + + fun getVideoList(): List { + return transaction { + ImageTable.selectAll() + .orderBy(ImageTable.upload_datetime, SortOrder.DESC) + .map { + ImageAnalyticsRequest( + object_name = "http://${SERVER_PATH}:9000/image/" + it[ImageTable.object_name], + upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]), + file_name = it[ImageTable.file_name], + resolution = it[ImageTable.resolution], + size = it[ImageTable.size], + cocoon_count = it[ImageTable.cocoon_count], + max_confidence = it[ImageTable.max_confidence], + min_confidence = it[ImageTable.min_confidence], + average_confidence = it[ImageTable.average_confidence], + other_info = it[ImageTable.other_info] as Map, + processing_time = formatLocalDateTimeToString(it[ImageTable.processing_time]) + ) + } + } + } + +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/database/UserDAO.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/UserDAO.kt new file mode 100644 index 0000000..4903365 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/UserDAO.kt @@ -0,0 +1,92 @@ +package ink.snowflake.server.utils.database + +import ink.snowflake.server.model.database.ImageTable +import ink.snowflake.server.model.database.UserTable +import ink.snowflake.server.model.response.UserInfo +import ink.snowflake.server.utils.formatLocalDateTimeToString +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction + +object UserDAO { + /** + * 根据 email 获取密码 + * @return 密码的 SHA-256 哈希值 + */ + fun getPasswordById(id: Int): String? { + return transaction { + // 查询指定 email 的用户并返回 passwordHash + UserTable + .selectAll().where { UserTable.id eq id } + .map { it[UserTable.passwordHash] } + .singleOrNull() // 如果没有找到用户,返回 null + } + } + + /** + * 根据 email 获取用户 ID + * @return 用户的 ID + */ + fun getUserIdByEmail(email: String): Int? { + return transaction { + // 查询指定 email 的用户并返回 id + UserTable + .selectAll().where { UserTable.email eq email } + .map { it[UserTable.id].value } + .singleOrNull() // 如果没有找到用户,返回 null + } + } + + /** + * 添加用户 + * @return 如果添加成功,返回 1;如果用户已存在,返回 0;如果添加失败,返回 -1 + */ + fun registerByEmailAndGetId(account: String, hashPassword: String): Int { + return transaction { + try { + val newUserId = UserTable + .insertAndGetId { + it[email] = account + it[passwordHash] = hashPassword + it[createdAt] = timestampLiteral(Clock.System.now()) + it[updatedAt] = timestampLiteral(Clock.System.now()) + } + // 如果插入成功,返回 userId + if (newUserId.value > 0) { + newUserId.value + } else { + // 插入失败,返回 -1 + -1 + } + } catch (e: Exception) { + // 如果发生异常(例如数据库连接问题),返回 -1 + e.printStackTrace() // 打印异常堆栈 + -1 + } + } + } + + fun getUserInfoByUserId(userId: Int?): UserInfo? { + if (userId == null) return null + + return transaction { + UserTable + .selectAll().where { UserTable.id eq userId } + .map { + UserInfo( + username = it[UserTable.username], + email = it[UserTable.email], + phone = it[UserTable.phone] + ) + } + .singleOrNull() + } + } + +} \ No newline at end of file diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/database/VideoDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/VideoDao.kt new file mode 100644 index 0000000..42e85a5 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/database/VideoDao.kt @@ -0,0 +1,92 @@ +package ink.snowflake.server.utils.database + +import ink.snowflake.server.model.database.VideoAnalyticsDetailTable +import ink.snowflake.server.model.database.VideoTable +import ink.snowflake.server.model.database.VideoTable.vATime +import ink.snowflake.server.model.database.VideoTable.vName +import ink.snowflake.server.model.request.VideoAnalyticsRequest +import ink.snowflake.server.model.request.VideoDetail +import ink.snowflake.server.model.response.VideoListResponse +import ink.snowflake.server.utils.formatLocalDateTimeToString +import kotlinx.datetime.toKotlinLocalDateTime +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Timestamp + +object VideoDao { + + fun insertVideoAnalyticsData(request: VideoAnalyticsRequest) { + return transaction { + // 插入视频信息 + val videoId = VideoTable.insertAndGetId { + it[vName] = request.v_name + it[vObjectName] = request.v_object_name + it[vFileName] = request.v_file_name + it[vStartDateTime] = + Timestamp.valueOf(request.v_start_datetime).toLocalDateTime().toKotlinLocalDateTime() + it[vDuration] = request.v_duration + it[vSize] = request.v_size + it[vVideoCodec] = request.v_video_codec + it[vAudioCodec] = request.v_audio_codec + it[vOverallBitRate] = request.v_overall_bit_rate + it[vResolution] = request.v_resolution + it[vATime] = Timestamp.valueOf(request.v_a_time).toLocalDateTime().toKotlinLocalDateTime() + it[vATotalPeople] = request.v_a_total_people + it[vACountPeople] = request.v_a_count_people + it[vAMaxStayTime] = request.v_a_max_stay_time + it[vAMaxAction] = request.v_a_max_action + it[vAAverageMaskedRatio] = request.v_a_average_masked_ratio + } + // 插入视频分析详情 + request.v_a_details.forEach { detail -> + VideoAnalyticsDetailTable.insert { + it[vId] = videoId.value + it[aTimeStamp] = detail.a_time_stamp + it[aTotalPeople] = detail.a_total_people + it[aTotalPeopleMasked] = detail.a_total_people_masked + it[aAction] = detail.a_action + } + } + } + } + + fun getVideoList():List { + return transaction { + VideoTable.selectAll() + .orderBy(vATime, SortOrder.DESC) + .map { + VideoListResponse( + v_id = it[VideoTable.id].value, + v_name = it[vName], + v_a_time = formatLocalDateTimeToString(it[vATime]) + ) + } + } + } + + fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? { + return transaction { + VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull() + } + } + + fun selectVideoDetailByVid(vId:Int): List{ + return transaction { + VideoAnalyticsDetailTable + .selectAll() + .where { VideoAnalyticsDetailTable.vId eq vId } + .map { + VideoDetail( + a_time_stamp = it[VideoAnalyticsDetailTable.aTimeStamp], + a_total_people = it[VideoAnalyticsDetailTable.aTotalPeople], + a_total_people_masked = it[VideoAnalyticsDetailTable.aTotalPeopleMasked], + a_action = it[VideoAnalyticsDetailTable.aAction] + ) + } + } + } +} \ No newline at end of file diff --git a/ktor/src/main/resources/application.yaml b/ktor/src/main/resources/application.yaml new file mode 100644 index 0000000..bb2cb5b --- /dev/null +++ b/ktor/src/main/resources/application.yaml @@ -0,0 +1,30 @@ +ktor: + application: + modules: + - ink.snowflake.server.ApplicationKt.module + deployment: + port: 8089 + connectionGroupSize: 20 + workerGroupSize: 50 + callGroupSize: 100 + shutdownGracePeriod: 20000 + shutdownTimeout: 30000 + security: + jwt: + audience: "snowflake-ink" + domain: "https://snowflake.ink/" + realm: "Snowflake Server" + secret: "secret_jwt" + database: + url: "jdbc:postgresql://localhost:5432/ktor" + driver: "org.postgresql.Driver" + user: "postgres" + password: "123456" + + mail: + smtp: + host: "smtp.exmail.qq.com" + port: 465 + user: "account@snowflake.ink" + password: "7ZYPc75xCViqSrCg" + diff --git a/ktor/src/test/kotlin/ink/snowflake/server/ApplicationTest.kt b/ktor/src/test/kotlin/ink/snowflake/server/ApplicationTest.kt new file mode 100644 index 0000000..26728f8 --- /dev/null +++ b/ktor/src/test/kotlin/ink/snowflake/server/ApplicationTest.kt @@ -0,0 +1,64 @@ +package ink.snowflake.server + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlin.test.* + +class ApplicationTest { + @Test + fun testRoot() = testApplication { + client.get("/").apply { + assertEquals(HttpStatusCode.OK, status) + assertEquals("Hello World!", bodyAsText()) + } + } + @Test + fun tasksCanBeFoundByPriority() = testApplication { + application { module() } + val response = client.get("/tasks/byPriority/Medium") + val body = response.bodyAsText() + assertEquals(HttpStatusCode.OK, response.status) + assertContains(body, "Mow the lawn") + assertContains(body, "Paint the fence") + } + + @Test + fun invalidPriorityProduces400() = testApplication { + application { module() } + val response = client.get("/tasks/byPriority/Invalid") + assertEquals(HttpStatusCode.BadRequest, response.status) + } + + @Test + fun unusedPriorityProduces404() = testApplication { + application { module() } + val response = client.get("/tasks/byPriority/Vital") + assertEquals(HttpStatusCode.NotFound, response.status) + } + + @Test + fun newTasksCanBeAdded() = testApplication { + application { module() } + val response1 = client.post("/tasksPost") { + header( + HttpHeaders.ContentType, + ContentType.Application.FormUrlEncoded.toString() + ) + setBody( + listOf( + "name" to "swimming", + "description" to "Go to the beach", + "priority" to "Low" + ).formUrlEncode() + ) + } + assertEquals(HttpStatusCode.NoContent, response1.status) + val response2 = client.get("/tasks") + assertEquals(HttpStatusCode.OK, response2.status) + val body = response2.bodyAsText() + assertContains(body, "swimming") + assertContains(body, "Go to the beach") + } +} \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml deleted file mode 100644 index 3ac5730..0000000 --- a/lefthook.yml +++ /dev/null @@ -1,42 +0,0 @@ -# EXAMPLE USAGE: -# -# Refer for explanation to following link: -# https://lefthook.dev/configuration/ -# -# pre-push: -# jobs: -# - name: packages audit -# tags: -# - frontend -# - security -# run: yarn audit -# -# - name: gems audit -# tags: -# - backend -# - security -# run: bundle audit -# -# pre-commit: -# parallel: true -# jobs: -# - run: yarn eslint {staged_files} -# glob: "*.{js,ts,jsx,tsx}" -# -# - name: rubocop -# glob: "*.rb" -# exclude: -# - config/application.rb -# - config/routes.rb -# run: bundle exec rubocop --force-exclusion {all_files} -# -# - name: govet -# files: git ls-files -m -# glob: "*.go" -# run: go vet {files} -# -# - script: "hello.js" -# runner: node -# -# - script: "hello.go" -# runner: go run diff --git a/vue/docs/.vitepress/config/index.mts.timestamp-1747016591710-9e47cd4ea35768.mjs b/vue/docs/.vitepress/config/index.mts.timestamp-1747016591710-9e47cd4ea35768.mjs new file mode 100644 index 0000000..91fda3e --- /dev/null +++ b/vue/docs/.vitepress/config/index.mts.timestamp-1747016591710-9e47cd4ea35768.mjs @@ -0,0 +1,877 @@ +// .vitepress/config/index.mts +import { withPwa } from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/@vite-pwa+vitepress@1.0.0_vite-plugin-pwa@1.0.0_vite@5.4.18_@types+node@22.15.3_less@4._fb43e8c1ebdf56d3d713af8997e2a4d9/node_modules/@vite-pwa/vitepress/dist/index.mjs"; +import { defineConfigWithTheme } from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.23.4_@types+node@22.15.3_async-validator@4.2.5_966d8f56fbb083318adba0029ed923d1/node_modules/vitepress/dist/node/index.js"; + +// .vitepress/config/en.mts +import { defineConfig } from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.23.4_@types+node@22.15.3_async-validator@4.2.5_966d8f56fbb083318adba0029ed923d1/node_modules/vitepress/dist/node/index.js"; + +// ../package.json +var version = "5.5.6"; + +// .vitepress/config/en.mts +var en = defineConfig({ + description: "Vben Admin & Enterprise level management system framework", + lang: "en-US", + themeConfig: { + darkModeSwitchLabel: "Theme", + darkModeSwitchTitle: "Switch to Dark Mode", + docFooter: { + next: "Next Page", + prev: "Previous Page" + }, + editLink: { + pattern: "https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path", + text: "Edit this page on GitHub" + }, + footer: { + copyright: `Copyright \xA9 2020-${(/* @__PURE__ */ new Date()).getFullYear()} Vben`, + message: "Released under the MIT License." + }, + langMenuLabel: "Language", + lastUpdated: { + formatOptions: { + dateStyle: "short", + timeStyle: "medium" + }, + text: "Last updated on" + }, + lightModeSwitchTitle: "Switch to Light Mode", + nav: nav(), + outline: { + label: "Navigate" + }, + returnToTopLabel: "Back to top", + sidebar: { + "/en/commercial/": { + base: "/en/commercial/", + items: sidebarCommercial() + }, + "/en/guide/": { base: "/en/guide/", items: sidebarGuide() } + } + } +}); +function sidebarGuide() { + return [ + { + collapsed: false, + text: "Introduction", + items: [ + { + link: "introduction/vben", + text: "About Vben Admin" + }, + { + link: "introduction/why", + text: "Why Choose Us?" + }, + { link: "introduction/quick-start", text: "Quick Start" }, + { link: "introduction/thin", text: "Lite Version" } + ] + }, + { + text: "Basics", + items: [ + { link: "essentials/concept", text: "Basic Concepts" }, + { link: "essentials/development", text: "Local Development" }, + { link: "essentials/route", text: "Routing and Menu" }, + { link: "essentials/settings", text: "Configuration" }, + { link: "essentials/icons", text: "Icons" }, + { link: "essentials/styles", text: "Styles" }, + { link: "essentials/external-module", text: "External Modules" }, + { link: "essentials/build", text: "Build and Deployment" }, + { link: "essentials/server", text: "Server Interaction and Data Mock" } + ] + }, + { + text: "Advanced", + items: [ + { link: "in-depth/login", text: "Login" }, + { link: "in-depth/theme", text: "Theme" }, + { link: "in-depth/access", text: "Access Control" }, + { link: "in-depth/locale", text: "Internationalization" }, + { link: "in-depth/features", text: "Common Features" }, + { link: "in-depth/check-updates", text: "Check Updates" }, + { link: "in-depth/loading", text: "Global Loading" }, + { link: "in-depth/ui-framework", text: "UI Framework Switching" } + ] + }, + { + text: "Engineering", + items: [ + { link: "project/standard", text: "Standards" }, + { link: "project/cli", text: "CLI" }, + { link: "project/dir", text: "Directory Explanation" }, + { link: "project/test", text: "Unit Testing" }, + { link: "project/tailwindcss", text: "Tailwind CSS" }, + { link: "project/changeset", text: "Changeset" }, + { link: "project/vite", text: "Vite Config" } + ] + }, + { + text: "Others", + items: [ + { link: "other/project-update", text: "Project Update" }, + { link: "other/remove-code", text: "Remove Code" }, + { link: "other/faq", text: "FAQ" } + ] + } + ]; +} +function sidebarCommercial() { + return [ + { + link: "community", + text: "Community" + }, + { + link: "technical-support", + text: "Technical-support" + }, + { + link: "customized", + text: "Customized" + } + ]; +} +function nav() { + return [ + { + activeMatch: "^/en/(guide|components)/", + text: "Doc", + items: [ + { + activeMatch: "^/en/guide/", + link: "/en/guide/introduction/vben", + text: "Guide" + }, + // { + // activeMatch: '^/en/components/', + // link: '/en/components/introduction', + // text: 'Components', + // }, + { + text: "Historical Versions", + items: [ + { + link: "https://doc.vvbin.cn", + text: "2.x Version Documentation" + } + ] + } + ] + }, + { + text: "Demo", + items: [ + { + text: "Vben Admin", + items: [ + { + link: "https://www.vben.pro", + text: "Demo Version" + }, + { + link: "https://ant.vben.pro", + text: "Ant Design Vue Version" + }, + { + link: "https://naive.vben.pro", + text: "Naive Version" + }, + { + link: "https://ele.vben.pro", + text: "Element Plus Version" + } + ] + }, + { + text: "Others", + items: [ + { + link: "https://vben.vvbin.cn", + text: "Vben Admin 2.x" + } + ] + } + ] + }, + { + text: version, + items: [ + { + link: "https://github.com/vbenjs/vue-vben-admin/releases", + text: "Changelog" + }, + { + link: "https://github.com/orgs/vbenjs/projects/5", + text: "Roadmap" + }, + { + link: "https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md", + text: "Contribution" + } + ] + }, + { + link: "/commercial/technical-support", + text: "\u{1F984} Tech Support" + }, + { + link: "/sponsor/personal", + text: "\u2728 Sponsor" + }, + { + link: "/commercial/community", + text: "\u{1F468}\u200D\u{1F466}\u200D\u{1F466} Community" + } + // { + // link: '/friend-links/', + // text: '🤝 Friend Links', + // }, + ]; +} + +// .vitepress/config/shared.mts +import { resolve } from "node:path"; +import { + viteArchiverPlugin, + viteVxeTableImportsPlugin +} from "file:///mnt/d/Mine/ICP/vue/internal/vite-config/dist/index.mjs"; +import { + GitChangelog, + GitChangelogMarkdownSection +} from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/@nolebase+vitepress-plugin-git-changelog@2.17.0_typescript@5.8.3_vitepress@1.6.3_@algol_62de00aa1032913364972b7ebf262193/node_modules/@nolebase/vitepress-plugin-git-changelog/dist/vite/index.mjs"; +import tailwind from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/lib/index.js"; +import { defineConfig as defineConfig3, postcssIsolateStyles } from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.23.4_@types+node@22.15.3_async-validator@4.2.5_966d8f56fbb083318adba0029ed923d1/node_modules/vitepress/dist/node/index.js"; +import { + groupIconMdPlugin, + groupIconVitePlugin +} from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/vitepress-plugin-group-icons@1.5.2/node_modules/vitepress-plugin-group-icons/dist/index.mjs"; + +// .vitepress/config/plugins/demo-preview.ts +import crypto from "node:crypto"; +import { readdirSync } from "node:fs"; +import { join } from "node:path"; +var rawPathRegexp = ( + // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict + /^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/ +); +function rawPathToToken(rawPath) { + const [ + filepath = "", + extension = "", + region = "", + lines = "", + lang = "", + rawTitle = "" + ] = (rawPathRegexp.exec(rawPath) || []).slice(1); + const title = rawTitle || filepath.split("/").pop() || ""; + return { extension, filepath, lang, lines, region, title }; +} +var demoPreviewPlugin = (md) => { + md.core.ruler.after("inline", "demo-preview", (state) => { + const insertComponentImport = (importString) => { + const index = state.tokens.findIndex( + (i) => i.type === "html_block" && i.content.match(/ +`; + state.tokens.splice(0, 0, importComponent); + } else { + if (state.tokens[index]) { + const content = state.tokens[index].content; + state.tokens[index].content = content.replace( + "", + `${importString} +` + ); + } + } + }; + const regex = /]*\sdir="([^"]*)"/g; + state.src = state.src.replaceAll(regex, (_match, dir) => { + const componentDir = join(process.cwd(), "src", dir).replaceAll( + "\\", + "/" + ); + let childFiles = []; + let dirExists = true; + try { + childFiles = readdirSync(componentDir, { + encoding: "utf8", + recursive: false, + withFileTypes: false + }) || []; + } catch { + dirExists = false; + } + if (!dirExists) { + return ""; + } + const uniqueWord = generateContentHash(componentDir); + const ComponentName = `DemoComponent_${uniqueWord}`; + insertComponentImport( + `import ${ComponentName} from '${componentDir}/index.vue'` + ); + const { path: _path } = state.env; + const index = state.tokens.findIndex((i) => i.content.match(regex)); + if (!state.tokens[index]) { + return ""; + } + const firstString = "index.vue"; + childFiles = childFiles.sort((a, b) => { + if (a === firstString) return -1; + if (b === firstString) return 1; + return a.localeCompare(b, "en", { sensitivity: "base" }); + }); + state.tokens[index].content = `<${ComponentName}/> + `; + const _dummyToken = new state.Token("", "", 0); + const tokenArray = []; + childFiles.forEach((filename) => { + const templateStart = new state.Token("html_inline", "", 0); + templateStart.content = `"; + tokenArray.push(templateEnd); + }); + const endTag = new state.Token("html_inline", "", 0); + endTag.content = ""; + tokenArray.push(endTag); + state.tokens.splice(index + 1, 0, ...tokenArray); + return ""; + }); + }); +}; +function generateContentHash(input, length = 10) { + const hash = crypto.createHash("sha256").update(input).digest("hex"); + return Number.parseInt(hash, 16).toString(36).slice(0, length); +} + +// .vitepress/config/zh.mts +import { defineConfig as defineConfig2 } from "file:///mnt/d/Mine/ICP/vue/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.23.4_@types+node@22.15.3_async-validator@4.2.5_966d8f56fbb083318adba0029ed923d1/node_modules/vitepress/dist/node/index.js"; +var zh = defineConfig2({ + description: "Vben Admin & \u4F01\u4E1A\u7EA7\u7BA1\u7406\u7CFB\u7EDF\u6846\u67B6", + lang: "zh-Hans", + themeConfig: { + darkModeSwitchLabel: "\u4E3B\u9898", + darkModeSwitchTitle: "\u5207\u6362\u5230\u6DF1\u8272\u6A21\u5F0F", + docFooter: { + next: "\u4E0B\u4E00\u9875", + prev: "\u4E0A\u4E00\u9875" + }, + editLink: { + pattern: "https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path", + text: "\u5728 GitHub \u4E0A\u7F16\u8F91\u6B64\u9875\u9762" + }, + footer: { + copyright: `Copyright \xA9 2020-${(/* @__PURE__ */ new Date()).getFullYear()} Vben`, + message: "\u57FA\u4E8E MIT \u8BB8\u53EF\u53D1\u5E03." + }, + langMenuLabel: "\u591A\u8BED\u8A00", + lastUpdated: { + formatOptions: { + dateStyle: "short", + timeStyle: "medium" + }, + text: "\u6700\u540E\u66F4\u65B0\u4E8E" + }, + lightModeSwitchTitle: "\u5207\u6362\u5230\u6D45\u8272\u6A21\u5F0F", + nav: nav2(), + outline: { + label: "\u9875\u9762\u5BFC\u822A" + }, + returnToTopLabel: "\u56DE\u5230\u9876\u90E8", + sidebar: { + "/commercial/": { base: "/commercial/", items: sidebarCommercial2() }, + "/components/": { base: "/components/", items: sidebarComponents() }, + "/guide/": { base: "/guide/", items: sidebarGuide2() } + }, + sidebarMenuLabel: "\u83DC\u5355" + } +}); +function sidebarGuide2() { + return [ + { + collapsed: false, + text: "\u7B80\u4ECB", + items: [ + { + link: "introduction/vben", + text: "\u5173\u4E8E Vben Admin" + }, + { + link: "introduction/why", + text: "\u4E3A\u4EC0\u4E48\u9009\u62E9\u6211\u4EEC?" + }, + { link: "introduction/quick-start", text: "\u5FEB\u901F\u5F00\u59CB" }, + { link: "introduction/thin", text: "\u7CBE\u7B80\u7248\u672C" }, + { + base: "/", + link: "components/introduction", + text: "\u7EC4\u4EF6\u6587\u6863" + } + ] + }, + { + text: "\u57FA\u7840", + items: [ + { link: "essentials/concept", text: "\u57FA\u7840\u6982\u5FF5" }, + { link: "essentials/development", text: "\u672C\u5730\u5F00\u53D1" }, + { link: "essentials/route", text: "\u8DEF\u7531\u548C\u83DC\u5355" }, + { link: "essentials/settings", text: "\u914D\u7F6E" }, + { link: "essentials/icons", text: "\u56FE\u6807" }, + { link: "essentials/styles", text: "\u6837\u5F0F" }, + { link: "essentials/external-module", text: "\u5916\u90E8\u6A21\u5757" }, + { link: "essentials/build", text: "\u6784\u5EFA\u4E0E\u90E8\u7F72" }, + { link: "essentials/server", text: "\u670D\u52A1\u7AEF\u4EA4\u4E92\u4E0E\u6570\u636EMock" } + ] + }, + { + text: "\u6DF1\u5165", + items: [ + { link: "in-depth/login", text: "\u767B\u5F55" }, + // { link: 'in-depth/layout', text: '布局' }, + { link: "in-depth/theme", text: "\u4E3B\u9898" }, + { link: "in-depth/access", text: "\u6743\u9650" }, + { link: "in-depth/locale", text: "\u56FD\u9645\u5316" }, + { link: "in-depth/features", text: "\u5E38\u7528\u529F\u80FD" }, + { link: "in-depth/check-updates", text: "\u68C0\u67E5\u66F4\u65B0" }, + { link: "in-depth/loading", text: "\u5168\u5C40loading" }, + { link: "in-depth/ui-framework", text: "\u7EC4\u4EF6\u5E93\u5207\u6362" } + ] + }, + { + text: "\u5DE5\u7A0B", + items: [ + { link: "project/standard", text: "\u89C4\u8303" }, + { link: "project/cli", text: "CLI" }, + { link: "project/dir", text: "\u76EE\u5F55\u8BF4\u660E" }, + { link: "project/test", text: "\u5355\u5143\u6D4B\u8BD5" }, + { link: "project/tailwindcss", text: "Tailwind CSS" }, + { link: "project/changeset", text: "Changeset" }, + { link: "project/vite", text: "Vite Config" } + ] + }, + { + text: "\u5176\u4ED6", + items: [ + { link: "other/project-update", text: "\u9879\u76EE\u66F4\u65B0" }, + { link: "other/remove-code", text: "\u79FB\u9664\u4EE3\u7801" }, + { link: "other/faq", text: "\u5E38\u89C1\u95EE\u9898" } + ] + } + ]; +} +function sidebarCommercial2() { + return [ + { + link: "community", + text: "\u4EA4\u6D41\u7FA4" + }, + { + link: "technical-support", + text: "\u6280\u672F\u652F\u6301" + }, + { + link: "customized", + text: "\u5B9A\u5236\u5F00\u53D1" + } + ]; +} +function sidebarComponents() { + return [ + { + text: "\u7EC4\u4EF6", + items: [ + { + link: "introduction", + text: "\u4ECB\u7ECD" + } + ] + }, + { + collapsed: false, + text: "\u5E03\u5C40\u7EC4\u4EF6", + items: [ + { + link: "layout-ui/page", + text: "Page \u9875\u9762" + } + ] + }, + { + collapsed: false, + text: "\u901A\u7528\u7EC4\u4EF6", + items: [ + { + link: "common-ui/vben-api-component", + text: "ApiComponent Api\u7EC4\u4EF6\u5305\u88C5\u5668" + }, + { + link: "common-ui/vben-alert", + text: "Alert \u8F7B\u91CF\u63D0\u793A\u6846" + }, + { + link: "common-ui/vben-modal", + text: "Modal \u6A21\u6001\u6846" + }, + { + link: "common-ui/vben-drawer", + text: "Drawer \u62BD\u5C49" + }, + { + link: "common-ui/vben-form", + text: "Form \u8868\u5355" + }, + { + link: "common-ui/vben-vxe-table", + text: "Vxe Table \u8868\u683C" + }, + { + link: "common-ui/vben-count-to-animator", + text: "CountToAnimator \u6570\u5B57\u52A8\u753B" + }, + { + link: "common-ui/vben-ellipsis-text", + text: "EllipsisText \u7701\u7565\u6587\u672C" + } + ] + } + ]; +} +function nav2() { + return [ + { + activeMatch: "^/(guide|components)/", + text: "\u6587\u6863", + items: [ + { + activeMatch: "^/guide/", + link: "/guide/introduction/vben", + text: "\u6307\u5357" + }, + { + activeMatch: "^/components/", + link: "/components/introduction", + text: "\u7EC4\u4EF6" + }, + { + text: "\u5386\u53F2\u7248\u672C", + items: [ + { + link: "https://doc.vvbin.cn", + text: "2.x\u7248\u672C\u6587\u6863" + } + ] + } + ] + }, + { + text: "\u6F14\u793A", + items: [ + { + text: "Vben Admin", + items: [ + { + link: "https://www.vben.pro", + text: "\u6F14\u793A\u7248\u672C" + }, + { + link: "https://ant.vben.pro", + text: "Ant Design Vue \u7248\u672C" + }, + { + link: "https://naive.vben.pro", + text: "Naive \u7248\u672C" + }, + { + link: "https://ele.vben.pro", + text: "Element Plus\u7248\u672C" + } + ] + }, + { + text: "\u5176\u4ED6", + items: [ + { + link: "https://vben.vvbin.cn", + text: "Vben Admin 2.x" + } + ] + } + ] + }, + { + text: version, + items: [ + { + link: "https://github.com/vbenjs/vue-vben-admin/releases", + text: "\u66F4\u65B0\u65E5\u5FD7" + }, + { + link: "https://github.com/orgs/vbenjs/projects/5", + text: "\u8DEF\u7EBF\u56FE" + }, + { + link: "https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md", + text: "\u8D21\u732E" + } + ] + }, + { + link: "/commercial/technical-support", + text: "\u{1F984} \u6280\u672F\u652F\u6301" + }, + { + link: "/sponsor/personal", + text: "\u2728 \u8D5E\u52A9" + }, + { + link: "/commercial/community", + text: "\u{1F468}\u200D\u{1F466}\u200D\u{1F466} \u4EA4\u6D41\u7FA4" + // items: [ + // { + // link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc', + // text: 'QQ频道', + // }, + // { + // link: 'https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=mjZmlhgVzzUxvdxllB6C1vHpX8O8QRL0&authKey=DBdFbBwERmfaKY95JvRWqLCJIRGJAmKyZbrpzZ41EKDMZ5SR6MfbjOBaaNRN73fr&noverify=0&group_code=4286109', + // text: 'QQ群', + // }, + // { + // link: 'https://discord.gg/VU62jTecad', + // text: 'Discord', + // }, + // ], + } + // { + // link: '/friend-links/', + // text: '🤝 友情链接', + // }, + ]; +} +var search = { + root: { + placeholder: "\u641C\u7D22\u6587\u6863", + translations: { + button: { + buttonAriaLabel: "\u641C\u7D22\u6587\u6863", + buttonText: "\u641C\u7D22\u6587\u6863" + }, + modal: { + errorScreen: { + helpText: "\u4F60\u53EF\u80FD\u9700\u8981\u68C0\u67E5\u4F60\u7684\u7F51\u7EDC\u8FDE\u63A5", + titleText: "\u65E0\u6CD5\u83B7\u53D6\u7ED3\u679C" + }, + footer: { + closeText: "\u5173\u95ED", + navigateText: "\u5207\u6362", + searchByText: "\u641C\u7D22\u63D0\u4F9B\u8005", + selectText: "\u9009\u62E9" + }, + noResultsScreen: { + noResultsText: "\u65E0\u6CD5\u627E\u5230\u76F8\u5173\u7ED3\u679C", + reportMissingResultsLinkText: "\u70B9\u51FB\u53CD\u9988", + reportMissingResultsText: "\u4F60\u8BA4\u4E3A\u8BE5\u67E5\u8BE2\u5E94\u8BE5\u6709\u7ED3\u679C\uFF1F", + suggestedQueryText: "\u4F60\u53EF\u4EE5\u5C1D\u8BD5\u67E5\u8BE2" + }, + searchBox: { + cancelButtonAriaLabel: "\u53D6\u6D88", + cancelButtonText: "\u53D6\u6D88", + resetButtonAriaLabel: "\u6E05\u9664\u67E5\u8BE2\u6761\u4EF6", + resetButtonTitle: "\u6E05\u9664\u67E5\u8BE2\u6761\u4EF6" + }, + startScreen: { + favoriteSearchesTitle: "\u6536\u85CF", + noRecentSearchesText: "\u6CA1\u6709\u641C\u7D22\u5386\u53F2", + recentSearchesTitle: "\u641C\u7D22\u5386\u53F2", + removeFavoriteSearchButtonTitle: "\u4ECE\u6536\u85CF\u4E2D\u79FB\u9664", + removeRecentSearchButtonTitle: "\u4ECE\u641C\u7D22\u5386\u53F2\u4E2D\u79FB\u9664", + saveRecentSearchButtonTitle: "\u4FDD\u5B58\u81F3\u641C\u7D22\u5386\u53F2" + } + } + } + } +}; + +// .vitepress/config/shared.mts +var shared = defineConfig3({ + appearance: "dark", + head: head(), + markdown: { + preConfig(md) { + md.use(demoPreviewPlugin); + md.use(groupIconMdPlugin); + } + }, + pwa: pwa(), + srcDir: "src", + themeConfig: { + i18nRouting: true, + logo: "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp", + search: { + options: { + locales: { + ...search + } + }, + provider: "local" + }, + siteTitle: "Vben Admin", + socialLinks: [ + { icon: "github", link: "https://github.com/vbenjs/vue-vben-admin" } + ] + }, + title: "Vben Admin", + vite: { + build: { + chunkSizeWarningLimit: Infinity, + minify: "terser" + }, + css: { + postcss: { + plugins: [ + tailwind(), + postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }) + ] + }, + preprocessorOptions: { + scss: { + api: "modern" + } + } + }, + json: { + stringify: true + }, + plugins: [ + GitChangelog({ + mapAuthors: [ + { + mapByNameAliases: ["Vben"], + name: "vben", + username: "anncwb" + }, + { + name: "vince", + username: "vince292007" + }, + { + name: "Li Kui", + username: "likui628" + } + ], + repoURL: () => "https://github.com/vbenjs/vue-vben-admin" + }), + GitChangelogMarkdownSection(), + viteArchiverPlugin({ outputDir: ".vitepress" }), + groupIconVitePlugin(), + await viteVxeTableImportsPlugin() + ], + server: { + fs: { + allow: ["../.."] + }, + host: true, + port: 6173 + }, + ssr: { + external: ["@vue/repl"] + } + } +}); +function head() { + return [ + ["meta", { content: "Vbenjs Team", name: "author" }], + [ + "meta", + { + content: "vben, vitejs, vite, shacdn-ui, vue", + name: "keywords" + } + ], + ["link", { href: "/favicon.ico", rel: "icon", type: "image/svg+xml" }], + [ + "meta", + { + content: "width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no", + name: "viewport" + } + ], + ["meta", { content: "vben admin docs", name: "keywords" }], + ["link", { href: "/favicon.ico", rel: "icon" }] + // [ + // 'script', + // { + // src: 'https://cdn.tailwindcss.com', + // }, + // ], + ]; +} +function pwa() { + return { + includeManifestIcons: false, + manifest: { + description: "Vben Admin is a modern admin dashboard template based on Vue 3. ", + icons: [ + { + sizes: "192x192", + src: "https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png", + type: "image/png" + }, + { + sizes: "512x512", + src: "https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png", + type: "image/png" + } + ], + id: "/", + name: "Vben Admin Doc", + short_name: "vben_admin_doc", + theme_color: "#ffffff" + }, + outDir: resolve(process.cwd(), ".vitepress/dist"), + registerType: "autoUpdate", + workbox: { + globPatterns: ["**/*.{css,js,html,svg,png,ico,txt,woff2}"], + maximumFileSizeToCacheInBytes: 5 * 1024 * 1024 + } + }; +} + +// .vitepress/config/index.mts +var index_default = withPwa( + defineConfigWithTheme({ + ...shared, + locales: { + en: { + label: "English", + lang: "en", + link: "/en/", + ...en + }, + root: { + label: "\u7B80\u4F53\u4E2D\u6587", + lang: "zh-CN", + ...zh + } + } + }) +); +export { + index_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLnZpdGVwcmVzcy9jb25maWcvaW5kZXgubXRzIiwgIi52aXRlcHJlc3MvY29uZmlnL2VuLm10cyIsICIuLi9wYWNrYWdlLmpzb24iLCAiLnZpdGVwcmVzcy9jb25maWcvc2hhcmVkLm10cyIsICIudml0ZXByZXNzL2NvbmZpZy9wbHVnaW5zL2RlbW8tcHJldmlldy50cyIsICIudml0ZXByZXNzL2NvbmZpZy96aC5tdHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWdcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9tbnQvZC9NaW5lL0lDUC92dWUvZG9jcy8udml0ZXByZXNzL2NvbmZpZy9pbmRleC5tdHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL21udC9kL01pbmUvSUNQL3Z1ZS9kb2NzLy52aXRlcHJlc3MvY29uZmlnL2luZGV4Lm10c1wiO2ltcG9ydCB7IHdpdGhQd2EgfSBmcm9tICdAdml0ZS1wd2Evdml0ZXByZXNzJztcbmltcG9ydCB7IGRlZmluZUNvbmZpZ1dpdGhUaGVtZSB9IGZyb20gJ3ZpdGVwcmVzcyc7XG5cbmltcG9ydCB7IGVuIH0gZnJvbSAnLi9lbi5tdHMnO1xuaW1wb3J0IHsgc2hhcmVkIH0gZnJvbSAnLi9zaGFyZWQubXRzJztcbmltcG9ydCB7IHpoIH0gZnJvbSAnLi96aC5tdHMnO1xuXG5leHBvcnQgZGVmYXVsdCB3aXRoUHdhKFxuICBkZWZpbmVDb25maWdXaXRoVGhlbWUoe1xuICAgIC4uLnNoYXJlZCxcbiAgICBsb2NhbGVzOiB7XG4gICAgICBlbjoge1xuICAgICAgICBsYWJlbDogJ0VuZ2xpc2gnLFxuICAgICAgICBsYW5nOiAnZW4nLFxuICAgICAgICBsaW5rOiAnL2VuLycsXG4gICAgICAgIC4uLmVuLFxuICAgICAgfSxcbiAgICAgIHJvb3Q6IHtcbiAgICAgICAgbGFiZWw6ICdcdTdCODBcdTRGNTNcdTRFMkRcdTY1ODcnLFxuICAgICAgICBsYW5nOiAnemgtQ04nLFxuICAgICAgICAuLi56aCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSksXG4pO1xuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWdcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9tbnQvZC9NaW5lL0lDUC92dWUvZG9jcy8udml0ZXByZXNzL2NvbmZpZy9lbi5tdHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL21udC9kL01pbmUvSUNQL3Z1ZS9kb2NzLy52aXRlcHJlc3MvY29uZmlnL2VuLm10c1wiO2ltcG9ydCB0eXBlIHsgRGVmYXVsdFRoZW1lIH0gZnJvbSAndml0ZXByZXNzJztcblxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZXByZXNzJztcblxuaW1wb3J0IHsgdmVyc2lvbiB9IGZyb20gJy4uLy4uLy4uL3BhY2thZ2UuanNvbic7XG5cbmV4cG9ydCBjb25zdCBlbiA9IGRlZmluZUNvbmZpZyh7XG4gIGRlc2NyaXB0aW9uOiAnVmJlbiBBZG1pbiAmIEVudGVycHJpc2UgbGV2ZWwgbWFuYWdlbWVudCBzeXN0ZW0gZnJhbWV3b3JrJyxcbiAgbGFuZzogJ2VuLVVTJyxcbiAgdGhlbWVDb25maWc6IHtcbiAgICBkYXJrTW9kZVN3aXRjaExhYmVsOiAnVGhlbWUnLFxuICAgIGRhcmtNb2RlU3dpdGNoVGl0bGU6ICdTd2l0Y2ggdG8gRGFyayBNb2RlJyxcbiAgICBkb2NGb290ZXI6IHtcbiAgICAgIG5leHQ6ICdOZXh0IFBhZ2UnLFxuICAgICAgcHJldjogJ1ByZXZpb3VzIFBhZ2UnLFxuICAgIH0sXG4gICAgZWRpdExpbms6IHtcbiAgICAgIHBhdHRlcm46XG4gICAgICAgICdodHRwczovL2dpdGh1Yi5jb20vdmJlbmpzL3Z1ZS12YmVuLWFkbWluL2VkaXQvbWFpbi9kb2NzL3NyYy86cGF0aCcsXG4gICAgICB0ZXh0OiAnRWRpdCB0aGlzIHBhZ2Ugb24gR2l0SHViJyxcbiAgICB9LFxuICAgIGZvb3Rlcjoge1xuICAgICAgY29weXJpZ2h0OiBgQ29weXJpZ2h0IFx1MDBBOSAyMDIwLSR7bmV3IERhdGUoKS5nZXRGdWxsWWVhcigpfSBWYmVuYCxcbiAgICAgIG1lc3NhZ2U6ICdSZWxlYXNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuJyxcbiAgICB9LFxuICAgIGxhbmdNZW51TGFiZWw6ICdMYW5ndWFnZScsXG4gICAgbGFzdFVwZGF0ZWQ6IHtcbiAgICAgIGZvcm1hdE9wdGlvbnM6IHtcbiAgICAgICAgZGF0ZVN0eWxlOiAnc2hvcnQnLFxuICAgICAgICB0aW1lU3R5bGU6ICdtZWRpdW0nLFxuICAgICAgfSxcbiAgICAgIHRleHQ6ICdMYXN0IHVwZGF0ZWQgb24nLFxuICAgIH0sXG4gICAgbGlnaHRNb2RlU3dpdGNoVGl0bGU6ICdTd2l0Y2ggdG8gTGlnaHQgTW9kZScsXG4gICAgbmF2OiBuYXYoKSxcbiAgICBvdXRsaW5lOiB7XG4gICAgICBsYWJlbDogJ05hdmlnYXRlJyxcbiAgICB9LFxuICAgIHJldHVyblRvVG9wTGFiZWw6ICdCYWNrIHRvIHRvcCcsXG4gICAgc2lkZWJhcjoge1xuICAgICAgJy9lbi9jb21tZXJjaWFsLyc6IHtcbiAgICAgICAgYmFzZTogJy9lbi9jb21tZXJjaWFsLycsXG4gICAgICAgIGl0ZW1zOiBzaWRlYmFyQ29tbWVyY2lhbCgpLFxuICAgICAgfSxcbiAgICAgICcvZW4vZ3VpZGUvJzogeyBiYXNlOiAnL2VuL2d1aWRlLycsIGl0ZW1zOiBzaWRlYmFyR3VpZGUoKSB9LFxuICAgIH0sXG4gIH0sXG59KTtcblxuZnVuY3Rpb24gc2lkZWJhckd1aWRlKCk6IERlZmF1bHRUaGVtZS5TaWRlYmFySXRlbVtdIHtcbiAgcmV0dXJuIFtcbiAgICB7XG4gICAgICBjb2xsYXBzZWQ6IGZhbHNlLFxuICAgICAgdGV4dDogJ0ludHJvZHVjdGlvbicsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2ludHJvZHVjdGlvbi92YmVuJyxcbiAgICAgICAgICB0ZXh0OiAnQWJvdXQgVmJlbiBBZG1pbicsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBsaW5rOiAnaW50cm9kdWN0aW9uL3doeScsXG4gICAgICAgICAgdGV4dDogJ1doeSBDaG9vc2UgVXM/JyxcbiAgICAgICAgfSxcbiAgICAgICAgeyBsaW5rOiAnaW50cm9kdWN0aW9uL3F1aWNrLXN0YXJ0JywgdGV4dDogJ1F1aWNrIFN0YXJ0JyB9LFxuICAgICAgICB7IGxpbms6ICdpbnRyb2R1Y3Rpb24vdGhpbicsIHRleHQ6ICdMaXRlIFZlcnNpb24nIH0sXG4gICAgICBdLFxuICAgIH0sXG4gICAge1xuICAgICAgdGV4dDogJ0Jhc2ljcycsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2NvbmNlcHQnLCB0ZXh0OiAnQmFzaWMgQ29uY2VwdHMnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvZGV2ZWxvcG1lbnQnLCB0ZXh0OiAnTG9jYWwgRGV2ZWxvcG1lbnQnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvcm91dGUnLCB0ZXh0OiAnUm91dGluZyBhbmQgTWVudScgfSxcbiAgICAgICAgeyBsaW5rOiAnZXNzZW50aWFscy9zZXR0aW5ncycsIHRleHQ6ICdDb25maWd1cmF0aW9uJyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2ljb25zJywgdGV4dDogJ0ljb25zJyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL3N0eWxlcycsIHRleHQ6ICdTdHlsZXMnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvZXh0ZXJuYWwtbW9kdWxlJywgdGV4dDogJ0V4dGVybmFsIE1vZHVsZXMnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvYnVpbGQnLCB0ZXh0OiAnQnVpbGQgYW5kIERlcGxveW1lbnQnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvc2VydmVyJywgdGV4dDogJ1NlcnZlciBJbnRlcmFjdGlvbiBhbmQgRGF0YSBNb2NrJyB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIHRleHQ6ICdBZHZhbmNlZCcsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7IGxpbms6ICdpbi1kZXB0aC9sb2dpbicsIHRleHQ6ICdMb2dpbicgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvdGhlbWUnLCB0ZXh0OiAnVGhlbWUnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2FjY2VzcycsIHRleHQ6ICdBY2Nlc3MgQ29udHJvbCcgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvbG9jYWxlJywgdGV4dDogJ0ludGVybmF0aW9uYWxpemF0aW9uJyB9LFxuICAgICAgICB7IGxpbms6ICdpbi1kZXB0aC9mZWF0dXJlcycsIHRleHQ6ICdDb21tb24gRmVhdHVyZXMnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2NoZWNrLXVwZGF0ZXMnLCB0ZXh0OiAnQ2hlY2sgVXBkYXRlcycgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvbG9hZGluZycsIHRleHQ6ICdHbG9iYWwgTG9hZGluZycgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvdWktZnJhbWV3b3JrJywgdGV4dDogJ1VJIEZyYW1ld29yayBTd2l0Y2hpbmcnIH0sXG4gICAgICBdLFxuICAgIH0sXG4gICAge1xuICAgICAgdGV4dDogJ0VuZ2luZWVyaW5nJyxcbiAgICAgIGl0ZW1zOiBbXG4gICAgICAgIHsgbGluazogJ3Byb2plY3Qvc3RhbmRhcmQnLCB0ZXh0OiAnU3RhbmRhcmRzJyB9LFxuICAgICAgICB7IGxpbms6ICdwcm9qZWN0L2NsaScsIHRleHQ6ICdDTEknIH0sXG4gICAgICAgIHsgbGluazogJ3Byb2plY3QvZGlyJywgdGV4dDogJ0RpcmVjdG9yeSBFeHBsYW5hdGlvbicgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC90ZXN0JywgdGV4dDogJ1VuaXQgVGVzdGluZycgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC90YWlsd2luZGNzcycsIHRleHQ6ICdUYWlsd2luZCBDU1MnIH0sXG4gICAgICAgIHsgbGluazogJ3Byb2plY3QvY2hhbmdlc2V0JywgdGV4dDogJ0NoYW5nZXNldCcgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC92aXRlJywgdGV4dDogJ1ZpdGUgQ29uZmlnJyB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIHRleHQ6ICdPdGhlcnMnLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAgeyBsaW5rOiAnb3RoZXIvcHJvamVjdC11cGRhdGUnLCB0ZXh0OiAnUHJvamVjdCBVcGRhdGUnIH0sXG4gICAgICAgIHsgbGluazogJ290aGVyL3JlbW92ZS1jb2RlJywgdGV4dDogJ1JlbW92ZSBDb2RlJyB9LFxuICAgICAgICB7IGxpbms6ICdvdGhlci9mYXEnLCB0ZXh0OiAnRkFRJyB9LFxuICAgICAgXSxcbiAgICB9LFxuICBdO1xufVxuXG5mdW5jdGlvbiBzaWRlYmFyQ29tbWVyY2lhbCgpOiBEZWZhdWx0VGhlbWUuU2lkZWJhckl0ZW1bXSB7XG4gIHJldHVybiBbXG4gICAge1xuICAgICAgbGluazogJ2NvbW11bml0eScsXG4gICAgICB0ZXh0OiAnQ29tbXVuaXR5JyxcbiAgICB9LFxuICAgIHtcbiAgICAgIGxpbms6ICd0ZWNobmljYWwtc3VwcG9ydCcsXG4gICAgICB0ZXh0OiAnVGVjaG5pY2FsLXN1cHBvcnQnLFxuICAgIH0sXG4gICAge1xuICAgICAgbGluazogJ2N1c3RvbWl6ZWQnLFxuICAgICAgdGV4dDogJ0N1c3RvbWl6ZWQnLFxuICAgIH0sXG4gIF07XG59XG5cbmZ1bmN0aW9uIG5hdigpOiBEZWZhdWx0VGhlbWUuTmF2SXRlbVtdIHtcbiAgcmV0dXJuIFtcbiAgICB7XG4gICAgICBhY3RpdmVNYXRjaDogJ14vZW4vKGd1aWRlfGNvbXBvbmVudHMpLycsXG4gICAgICB0ZXh0OiAnRG9jJyxcbiAgICAgIGl0ZW1zOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBhY3RpdmVNYXRjaDogJ14vZW4vZ3VpZGUvJyxcbiAgICAgICAgICBsaW5rOiAnL2VuL2d1aWRlL2ludHJvZHVjdGlvbi92YmVuJyxcbiAgICAgICAgICB0ZXh0OiAnR3VpZGUnLFxuICAgICAgICB9LFxuICAgICAgICAvLyB7XG4gICAgICAgIC8vICAgYWN0aXZlTWF0Y2g6ICdeL2VuL2NvbXBvbmVudHMvJyxcbiAgICAgICAgLy8gICBsaW5rOiAnL2VuL2NvbXBvbmVudHMvaW50cm9kdWN0aW9uJyxcbiAgICAgICAgLy8gICB0ZXh0OiAnQ29tcG9uZW50cycsXG4gICAgICAgIC8vIH0sXG4gICAgICAgIHtcbiAgICAgICAgICB0ZXh0OiAnSGlzdG9yaWNhbCBWZXJzaW9ucycsXG4gICAgICAgICAgaXRlbXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbGluazogJ2h0dHBzOi8vZG9jLnZ2YmluLmNuJyxcbiAgICAgICAgICAgICAgdGV4dDogJzIueCBWZXJzaW9uIERvY3VtZW50YXRpb24nLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICBdLFxuICAgICAgICB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIHRleHQ6ICdEZW1vJyxcbiAgICAgIGl0ZW1zOiBbXG4gICAgICAgIHtcbiAgICAgICAgICB0ZXh0OiAnVmJlbiBBZG1pbicsXG4gICAgICAgICAgaXRlbXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbGluazogJ2h0dHBzOi8vd3d3LnZiZW4ucHJvJyxcbiAgICAgICAgICAgICAgdGV4dDogJ0RlbW8gVmVyc2lvbicsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly9hbnQudmJlbi5wcm8nLFxuICAgICAgICAgICAgICB0ZXh0OiAnQW50IERlc2lnbiBWdWUgVmVyc2lvbicsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly9uYWl2ZS52YmVuLnBybycsXG4gICAgICAgICAgICAgIHRleHQ6ICdOYWl2ZSBWZXJzaW9uJyxcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgIGxpbms6ICdodHRwczovL2VsZS52YmVuLnBybycsXG4gICAgICAgICAgICAgIHRleHQ6ICdFbGVtZW50IFBsdXMgVmVyc2lvbicsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICB0ZXh0OiAnT3RoZXJzJyxcbiAgICAgICAgICBpdGVtczogW1xuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly92YmVuLnZ2YmluLmNuJyxcbiAgICAgICAgICAgICAgdGV4dDogJ1ZiZW4gQWRtaW4gMi54JyxcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgXSxcbiAgICAgICAgfSxcbiAgICAgIF0sXG4gICAgfSxcbiAgICB7XG4gICAgICB0ZXh0OiB2ZXJzaW9uLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdodHRwczovL2dpdGh1Yi5jb20vdmJlbmpzL3Z1ZS12YmVuLWFkbWluL3JlbGVhc2VzJyxcbiAgICAgICAgICB0ZXh0OiAnQ2hhbmdlbG9nJyxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdodHRwczovL2dpdGh1Yi5jb20vb3Jncy92YmVuanMvcHJvamVjdHMvNScsXG4gICAgICAgICAgdGV4dDogJ1JvYWRtYXAnLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2h0dHBzOi8vZ2l0aHViLmNvbS92YmVuanMvdnVlLXZiZW4tYWRtaW4vYmxvYi9tYWluLy5naXRodWIvY29udHJpYnV0aW5nLm1kJyxcbiAgICAgICAgICB0ZXh0OiAnQ29udHJpYnV0aW9uJyxcbiAgICAgICAgfSxcbiAgICAgIF0sXG4gICAgfSxcbiAgICB7XG4gICAgICBsaW5rOiAnL2NvbW1lcmNpYWwvdGVjaG5pY2FsLXN1cHBvcnQnLFxuICAgICAgdGV4dDogJ1x1RDgzRVx1REQ4NCBUZWNoIFN1cHBvcnQnLFxuICAgIH0sXG4gICAge1xuICAgICAgbGluazogJy9zcG9uc29yL3BlcnNvbmFsJyxcbiAgICAgIHRleHQ6ICdcdTI3MjggU3BvbnNvcicsXG4gICAgfSxcbiAgICB7XG4gICAgICBsaW5rOiAnL2NvbW1lcmNpYWwvY29tbXVuaXR5JyxcbiAgICAgIHRleHQ6ICdcdUQ4M0RcdURDNjhcdTIwMERcdUQ4M0RcdURDNjZcdTIwMERcdUQ4M0RcdURDNjYgQ29tbXVuaXR5JyxcbiAgICB9LFxuICAgIC8vIHtcbiAgICAvLyAgIGxpbms6ICcvZnJpZW5kLWxpbmtzLycsXG4gICAgLy8gICB0ZXh0OiAnXHVEODNFXHVERDFEIEZyaWVuZCBMaW5rcycsXG4gICAgLy8gfSxcbiAgXTtcbn1cbiIsICJ7XG4gIFwibmFtZVwiOiBcInZiZW4tYWRtaW4tbW9ub3JlcG9cIixcbiAgXCJ2ZXJzaW9uXCI6IFwiNS41LjZcIixcbiAgXCJwcml2YXRlXCI6IHRydWUsXG4gIFwia2V5d29yZHNcIjogW1xuICAgIFwibW9ub3JlcG9cIixcbiAgICBcInR1cmJvXCIsXG4gICAgXCJ2YmVuXCIsXG4gICAgXCJ2YmVuIGFkbWluXCIsXG4gICAgXCJ2YmVuIHByb1wiLFxuICAgIFwidnVlXCIsXG4gICAgXCJ2dWUgYWRtaW5cIixcbiAgICBcInZ1ZSB2YmVuIGFkbWluXCIsXG4gICAgXCJ2dWUgdmJlbiBhZG1pbiBwcm9cIixcbiAgICBcInZ1ZTNcIlxuICBdLFxuICBcImhvbWVwYWdlXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL3ZiZW5qcy92dWUtdmJlbi1hZG1pblwiLFxuICBcImJ1Z3NcIjogXCJodHRwczovL2dpdGh1Yi5jb20vdmJlbmpzL3Z1ZS12YmVuLWFkbWluL2lzc3Vlc1wiLFxuICBcInJlcG9zaXRvcnlcIjogXCJ2YmVuanMvdnVlLXZiZW4tYWRtaW4uZ2l0XCIsXG4gIFwibGljZW5zZVwiOiBcIk1JVFwiLFxuICBcImF1dGhvclwiOiB7XG4gICAgXCJuYW1lXCI6IFwidmJlblwiLFxuICAgIFwiZW1haWxcIjogXCJhbm4udmJlbkBnbWFpbC5jb21cIixcbiAgICBcInVybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9hbm5jd2JcIlxuICB9LFxuICBcInR5cGVcIjogXCJtb2R1bGVcIixcbiAgXCJzY3JpcHRzXCI6IHtcbiAgICBcImJ1aWxkXCI6IFwiY3Jvc3MtZW52IE5PREVfT1BUSU9OUz0tLW1heC1vbGQtc3BhY2Utc2l6ZT04MTkyIHR1cmJvIGJ1aWxkXCIsXG4gICAgXCJidWlsZDphbmFseXplXCI6IFwidHVyYm8gYnVpbGQ6YW5hbHl6ZVwiLFxuICAgIFwiYnVpbGQ6YW50ZFwiOiBcInBucG0gcnVuIGJ1aWxkIC0tZmlsdGVyPUB2YmVuL3dlYi1hbnRkXCIsXG4gICAgXCJidWlsZDpkb2NrZXJcIjogXCIuL3NjcmlwdHMvZGVwbG95L2J1aWxkLWxvY2FsLWRvY2tlci1pbWFnZS5zaFwiLFxuICAgIFwiYnVpbGQ6ZG9jc1wiOiBcInBucG0gcnVuIGJ1aWxkIC0tZmlsdGVyPUB2YmVuL2RvY3NcIixcbiAgICBcImJ1aWxkOmVsZVwiOiBcInBucG0gcnVuIGJ1aWxkIC0tZmlsdGVyPUB2YmVuL3dlYi1lbGVcIixcbiAgICBcImJ1aWxkOm5haXZlXCI6IFwicG5wbSBydW4gYnVpbGQgLS1maWx0ZXI9QHZiZW4vd2ViLW5haXZlXCIsXG4gICAgXCJidWlsZDpwbGF5XCI6IFwicG5wbSBydW4gYnVpbGQgLS1maWx0ZXI9QHZiZW4vcGxheWdyb3VuZFwiLFxuICAgIFwiY2hhbmdlc2V0XCI6IFwicG5wbSBleGVjIGNoYW5nZXNldFwiLFxuICAgIFwiY2hlY2tcIjogXCJwbnBtIHJ1biBjaGVjazpjaXJjdWxhciAmJiBwbnBtIHJ1biBjaGVjazpkZXAgJiYgcG5wbSBydW4gY2hlY2s6dHlwZSAmJiBwbnBtIGNoZWNrOmNzcGVsbFwiLFxuICAgIFwiY2hlY2s6Y2lyY3VsYXJcIjogXCJ2c2ggY2hlY2stY2lyY3VsYXJcIixcbiAgICBcImNoZWNrOmNzcGVsbFwiOiBcImNzcGVsbCBsaW50ICoqLyoudHMgKiovUkVBRE1FLm1kIC5jaGFuZ2VzZXQvKi5tZCAtLW5vLXByb2dyZXNzXCIsXG4gICAgXCJjaGVjazpkZXBcIjogXCJ2c2ggY2hlY2stZGVwXCIsXG4gICAgXCJjaGVjazp0eXBlXCI6IFwidHVyYm8gcnVuIHR5cGVjaGVja1wiLFxuICAgIFwiY2xlYW5cIjogXCJub2RlIC4vc2NyaXB0cy9jbGVhbi5tanNcIixcbiAgICBcImNvbW1pdFwiOiBcImN6Z1wiLFxuICAgIFwiZGV2XCI6IFwidHVyYm8tcnVuIGRldlwiLFxuICAgIFwiZGV2OmFudGRcIjogXCJwbnBtIC1GIEB2YmVuL3dlYi1hbnRkIHJ1biBkZXZcIixcbiAgICBcImRldjpkb2NzXCI6IFwicG5wbSAtRiBAdmJlbi9kb2NzIHJ1biBkZXZcIixcbiAgICBcImRldjpwbGF5XCI6IFwicG5wbSAtRiBAdmJlbi9wbGF5Z3JvdW5kIHJ1biBkZXZcIixcbiAgICBcImZvcm1hdFwiOiBcInZzaCBsaW50IC0tZm9ybWF0XCIsXG4gICAgXCJsaW50XCI6IFwidnNoIGxpbnRcIixcbiAgICBcInBvc3RpbnN0YWxsXCI6IFwicG5wbSAtciBydW4gc3R1YiAtLWlmLXByZXNlbnRcIixcbiAgICBcInByZWluc3RhbGxcIjogXCJucHggb25seS1hbGxvdyBwbnBtXCIsXG4gICAgXCJwcmV2aWV3XCI6IFwidHVyYm8tcnVuIHByZXZpZXdcIixcbiAgICBcInB1YmxpbnRcIjogXCJ2c2ggcHVibGludFwiLFxuICAgIFwicmVpbnN0YWxsXCI6IFwicG5wbSBjbGVhbiAtLWRlbC1sb2NrICYmIHBucG0gaW5zdGFsbFwiLFxuICAgIFwidGVzdDp1bml0XCI6IFwidml0ZXN0IHJ1biAtLWRvbVwiLFxuICAgIFwidGVzdDplMmVcIjogXCJ0dXJibyBydW4gdGVzdDplMmVcIixcbiAgICBcInVwZGF0ZTpkZXBzXCI6IFwibnB4IHRhemUgLXIgLXdcIixcbiAgICBcInZlcnNpb25cIjogXCJwbnBtIGV4ZWMgY2hhbmdlc2V0IHZlcnNpb24gJiYgcG5wbSBpbnN0YWxsIC0tbm8tZnJvemVuLWxvY2tmaWxlXCIsXG4gICAgXCJjYXRhbG9nXCI6IFwicG5weCBjb2RlbW9kIHBucG0vY2F0YWxvZ1wiXG4gIH0sXG4gIFwiZGV2RGVwZW5kZW5jaWVzXCI6IHtcbiAgICBcIkBjaGFuZ2VzZXRzL2NoYW5nZWxvZy1naXRodWJcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiQGNoYW5nZXNldHMvY2xpXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcIkBwbGF5d3JpZ2h0L3Rlc3RcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiQHR5cGVzL25vZGVcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiQHZiZW4vY29tbWl0bGludC1jb25maWdcIjogXCJ3b3Jrc3BhY2U6KlwiLFxuICAgIFwiQHZiZW4vZXNsaW50LWNvbmZpZ1wiOiBcIndvcmtzcGFjZToqXCIsXG4gICAgXCJAdmJlbi9wcmV0dGllci1jb25maWdcIjogXCJ3b3Jrc3BhY2U6KlwiLFxuICAgIFwiQHZiZW4vc3R5bGVsaW50LWNvbmZpZ1wiOiBcIndvcmtzcGFjZToqXCIsXG4gICAgXCJAdmJlbi90YWlsd2luZC1jb25maWdcIjogXCJ3b3Jrc3BhY2U6KlwiLFxuICAgIFwiQHZiZW4vdHNjb25maWdcIjogXCJ3b3Jrc3BhY2U6KlwiLFxuICAgIFwiQHZiZW4vdHVyYm8tcnVuXCI6IFwid29ya3NwYWNlOipcIixcbiAgICBcIkB2YmVuL3ZpdGUtY29uZmlnXCI6IFwid29ya3NwYWNlOipcIixcbiAgICBcIkB2YmVuL3ZzaFwiOiBcIndvcmtzcGFjZToqXCIsXG4gICAgXCJAdml0ZWpzL3BsdWdpbi12dWVcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiQHZpdGVqcy9wbHVnaW4tdnVlLWpzeFwiOiBcImNhdGFsb2c6XCIsXG4gICAgXCJAdnVlL3Rlc3QtdXRpbHNcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiYXV0b3ByZWZpeGVyXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcImNyb3NzLWVudlwiOiBcImNhdGFsb2c6XCIsXG4gICAgXCJjc3BlbGxcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwiaGFwcHktZG9tXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcImlzLWNpXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcImxlZnRob29rXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcInBsYXl3cmlnaHRcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwicmltcmFmXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcInRhaWx3aW5kY3NzXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcInR1cmJvXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcInR5cGVzY3JpcHRcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwidW5idWlsZFwiOiBcImNhdGFsb2c6XCIsXG4gICAgXCJ2aXRlXCI6IFwiY2F0YWxvZzpcIixcbiAgICBcInZpdGVzdFwiOiBcImNhdGFsb2c6XCIsXG4gICAgXCJ2dWVcIjogXCJjYXRhbG9nOlwiLFxuICAgIFwidnVlLXRzY1wiOiBcImNhdGFsb2c6XCJcbiAgfSxcbiAgXCJlbmdpbmVzXCI6IHtcbiAgICBcIm5vZGVcIjogXCI+PTIwLjEwLjBcIixcbiAgICBcInBucG1cIjogXCI+PTkuMTIuMFwiXG4gIH0sXG4gIFwicGFja2FnZU1hbmFnZXJcIjogXCJwbnBtQDEwLjEwLjBcIixcbiAgXCJwbnBtXCI6IHtcbiAgICBcInBlZXJEZXBlbmRlbmN5UnVsZXNcIjoge1xuICAgICAgXCJhbGxvd2VkVmVyc2lvbnNcIjoge1xuICAgICAgICBcImVzbGludFwiOiBcIipcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJvdmVycmlkZXNcIjoge1xuICAgICAgXCJAYXN0LWdyZXAvbmFwaVwiOiBcImNhdGFsb2c6XCIsXG4gICAgICBcIkBjdHJsL3Rpbnljb2xvclwiOiBcImNhdGFsb2c6XCIsXG4gICAgICBcImNsc3hcIjogXCJjYXRhbG9nOlwiLFxuICAgICAgXCJlc2J1aWxkXCI6IFwiMC4yNS4zXCIsXG4gICAgICBcInBpbmlhXCI6IFwiY2F0YWxvZzpcIixcbiAgICAgIFwidnVlXCI6IFwiY2F0YWxvZzpcIlxuICAgIH0sXG4gICAgXCJuZXZlckJ1aWx0RGVwZW5kZW5jaWVzXCI6IFtcbiAgICAgIFwiY2FudmFzXCIsXG4gICAgICBcIm5vZGUtZ3lwXCJcbiAgICBdXG4gIH1cbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL21udC9kL01pbmUvSUNQL3Z1ZS9kb2NzLy52aXRlcHJlc3MvY29uZmlnXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWcvc2hhcmVkLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWcvc2hhcmVkLm10c1wiO2ltcG9ydCB0eXBlIHsgUHdhT3B0aW9ucyB9IGZyb20gJ0B2aXRlLXB3YS92aXRlcHJlc3MnO1xuaW1wb3J0IHR5cGUgeyBIZWFkQ29uZmlnIH0gZnJvbSAndml0ZXByZXNzJztcblxuaW1wb3J0IHsgcmVzb2x2ZSB9IGZyb20gJ25vZGU6cGF0aCc7XG5cbmltcG9ydCB7XG4gIHZpdGVBcmNoaXZlclBsdWdpbixcbiAgdml0ZVZ4ZVRhYmxlSW1wb3J0c1BsdWdpbixcbn0gZnJvbSAnQHZiZW4vdml0ZS1jb25maWcnO1xuXG5pbXBvcnQge1xuICBHaXRDaGFuZ2Vsb2csXG4gIEdpdENoYW5nZWxvZ01hcmtkb3duU2VjdGlvbixcbn0gZnJvbSAnQG5vbGViYXNlL3ZpdGVwcmVzcy1wbHVnaW4tZ2l0LWNoYW5nZWxvZy92aXRlJztcbmltcG9ydCB0YWlsd2luZCBmcm9tICd0YWlsd2luZGNzcyc7XG5pbXBvcnQgeyBkZWZpbmVDb25maWcsIHBvc3Rjc3NJc29sYXRlU3R5bGVzIH0gZnJvbSAndml0ZXByZXNzJztcbmltcG9ydCB7XG4gIGdyb3VwSWNvbk1kUGx1Z2luLFxuICBncm91cEljb25WaXRlUGx1Z2luLFxufSBmcm9tICd2aXRlcHJlc3MtcGx1Z2luLWdyb3VwLWljb25zJztcblxuaW1wb3J0IHsgZGVtb1ByZXZpZXdQbHVnaW4gfSBmcm9tICcuL3BsdWdpbnMvZGVtby1wcmV2aWV3JztcbmltcG9ydCB7IHNlYXJjaCBhcyB6aFNlYXJjaCB9IGZyb20gJy4vemgubXRzJztcblxuZXhwb3J0IGNvbnN0IHNoYXJlZCA9IGRlZmluZUNvbmZpZyh7XG4gIGFwcGVhcmFuY2U6ICdkYXJrJyxcbiAgaGVhZDogaGVhZCgpLFxuICBtYXJrZG93bjoge1xuICAgIHByZUNvbmZpZyhtZCkge1xuICAgICAgbWQudXNlKGRlbW9QcmV2aWV3UGx1Z2luKTtcbiAgICAgIG1kLnVzZShncm91cEljb25NZFBsdWdpbik7XG4gICAgfSxcbiAgfSxcbiAgcHdhOiBwd2EoKSxcbiAgc3JjRGlyOiAnc3JjJyxcbiAgdGhlbWVDb25maWc6IHtcbiAgICBpMThuUm91dGluZzogdHJ1ZSxcbiAgICBsb2dvOiAnaHR0cHM6Ly91bnBrZy5jb20vQHZiZW5qcy9zdGF0aWMtc291cmNlQDAuMS43L3NvdXJjZS9sb2dvLXYxLndlYnAnLFxuICAgIHNlYXJjaDoge1xuICAgICAgb3B0aW9uczoge1xuICAgICAgICBsb2NhbGVzOiB7XG4gICAgICAgICAgLi4uemhTZWFyY2gsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgcHJvdmlkZXI6ICdsb2NhbCcsXG4gICAgfSxcbiAgICBzaXRlVGl0bGU6ICdWYmVuIEFkbWluJyxcbiAgICBzb2NpYWxMaW5rczogW1xuICAgICAgeyBpY29uOiAnZ2l0aHViJywgbGluazogJ2h0dHBzOi8vZ2l0aHViLmNvbS92YmVuanMvdnVlLXZiZW4tYWRtaW4nIH0sXG4gICAgXSxcbiAgfSxcbiAgdGl0bGU6ICdWYmVuIEFkbWluJyxcbiAgdml0ZToge1xuICAgIGJ1aWxkOiB7XG4gICAgICBjaHVua1NpemVXYXJuaW5nTGltaXQ6IEluZmluaXR5LFxuICAgICAgbWluaWZ5OiAndGVyc2VyJyxcbiAgICB9LFxuICAgIGNzczoge1xuICAgICAgcG9zdGNzczoge1xuICAgICAgICBwbHVnaW5zOiBbXG4gICAgICAgICAgdGFpbHdpbmQoKSxcbiAgICAgICAgICBwb3N0Y3NzSXNvbGF0ZVN0eWxlcyh7IGluY2x1ZGVGaWxlczogWy92cC1kb2NcXC5jc3MvXSB9KSxcbiAgICAgICAgXSxcbiAgICAgIH0sXG4gICAgICBwcmVwcm9jZXNzb3JPcHRpb25zOiB7XG4gICAgICAgIHNjc3M6IHtcbiAgICAgICAgICBhcGk6ICdtb2Rlcm4nLFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9LFxuICAgIGpzb246IHtcbiAgICAgIHN0cmluZ2lmeTogdHJ1ZSxcbiAgICB9LFxuICAgIHBsdWdpbnM6IFtcbiAgICAgIEdpdENoYW5nZWxvZyh7XG4gICAgICAgIG1hcEF1dGhvcnM6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBtYXBCeU5hbWVBbGlhc2VzOiBbJ1ZiZW4nXSxcbiAgICAgICAgICAgIG5hbWU6ICd2YmVuJyxcbiAgICAgICAgICAgIHVzZXJuYW1lOiAnYW5uY3diJyxcbiAgICAgICAgICB9LFxuICAgICAgICAgIHtcbiAgICAgICAgICAgIG5hbWU6ICd2aW5jZScsXG4gICAgICAgICAgICB1c2VybmFtZTogJ3ZpbmNlMjkyMDA3JyxcbiAgICAgICAgICB9LFxuICAgICAgICAgIHtcbiAgICAgICAgICAgIG5hbWU6ICdMaSBLdWknLFxuICAgICAgICAgICAgdXNlcm5hbWU6ICdsaWt1aTYyOCcsXG4gICAgICAgICAgfSxcbiAgICAgICAgXSxcbiAgICAgICAgcmVwb1VSTDogKCkgPT4gJ2h0dHBzOi8vZ2l0aHViLmNvbS92YmVuanMvdnVlLXZiZW4tYWRtaW4nLFxuICAgICAgfSksXG4gICAgICBHaXRDaGFuZ2Vsb2dNYXJrZG93blNlY3Rpb24oKSxcbiAgICAgIHZpdGVBcmNoaXZlclBsdWdpbih7IG91dHB1dERpcjogJy52aXRlcHJlc3MnIH0pLFxuICAgICAgZ3JvdXBJY29uVml0ZVBsdWdpbigpLFxuICAgICAgYXdhaXQgdml0ZVZ4ZVRhYmxlSW1wb3J0c1BsdWdpbigpLFxuICAgIF0sXG4gICAgc2VydmVyOiB7XG4gICAgICBmczoge1xuICAgICAgICBhbGxvdzogWycuLi8uLiddLFxuICAgICAgfSxcbiAgICAgIGhvc3Q6IHRydWUsXG4gICAgICBwb3J0OiA2MTczLFxuICAgIH0sXG5cbiAgICBzc3I6IHtcbiAgICAgIGV4dGVybmFsOiBbJ0B2dWUvcmVwbCddLFxuICAgIH0sXG4gIH0sXG59KTtcblxuZnVuY3Rpb24gaGVhZCgpOiBIZWFkQ29uZmlnW10ge1xuICByZXR1cm4gW1xuICAgIFsnbWV0YScsIHsgY29udGVudDogJ1ZiZW5qcyBUZWFtJywgbmFtZTogJ2F1dGhvcicgfV0sXG4gICAgW1xuICAgICAgJ21ldGEnLFxuICAgICAge1xuICAgICAgICBjb250ZW50OiAndmJlbiwgdml0ZWpzLCB2aXRlLCBzaGFjZG4tdWksIHZ1ZScsXG4gICAgICAgIG5hbWU6ICdrZXl3b3JkcycsXG4gICAgICB9LFxuICAgIF0sXG4gICAgWydsaW5rJywgeyBocmVmOiAnL2Zhdmljb24uaWNvJywgcmVsOiAnaWNvbicsIHR5cGU6ICdpbWFnZS9zdmcreG1sJyB9XSxcbiAgICBbXG4gICAgICAnbWV0YScsXG4gICAgICB7XG4gICAgICAgIGNvbnRlbnQ6XG4gICAgICAgICAgJ3dpZHRoPWRldmljZS13aWR0aCxpbml0aWFsLXNjYWxlPTEsbWluaW11bS1zY2FsZT0xLjAsbWF4aW11bS1zY2FsZT0xLjAsdXNlci1zY2FsYWJsZT1ubycsXG4gICAgICAgIG5hbWU6ICd2aWV3cG9ydCcsXG4gICAgICB9LFxuICAgIF0sXG4gICAgWydtZXRhJywgeyBjb250ZW50OiAndmJlbiBhZG1pbiBkb2NzJywgbmFtZTogJ2tleXdvcmRzJyB9XSxcbiAgICBbJ2xpbmsnLCB7IGhyZWY6ICcvZmF2aWNvbi5pY28nLCByZWw6ICdpY29uJyB9XSxcbiAgICAvLyBbXG4gICAgLy8gICAnc2NyaXB0JyxcbiAgICAvLyAgIHtcbiAgICAvLyAgICAgc3JjOiAnaHR0cHM6Ly9jZG4udGFpbHdpbmRjc3MuY29tJyxcbiAgICAvLyAgIH0sXG4gICAgLy8gXSxcbiAgXTtcbn1cblxuZnVuY3Rpb24gcHdhKCk6IFB3YU9wdGlvbnMge1xuICByZXR1cm4ge1xuICAgIGluY2x1ZGVNYW5pZmVzdEljb25zOiBmYWxzZSxcbiAgICBtYW5pZmVzdDoge1xuICAgICAgZGVzY3JpcHRpb246XG4gICAgICAgICdWYmVuIEFkbWluIGlzIGEgbW9kZXJuIGFkbWluIGRhc2hib2FyZCB0ZW1wbGF0ZSBiYXNlZCBvbiBWdWUgMy4gJyxcbiAgICAgIGljb25zOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBzaXplczogJzE5MngxOTInLFxuICAgICAgICAgIHNyYzogJ2h0dHBzOi8vdW5wa2cuY29tL0B2YmVuanMvc3RhdGljLXNvdXJjZUAwLjEuNy9zb3VyY2UvcHdhLWljb24tMTkyLnBuZycsXG4gICAgICAgICAgdHlwZTogJ2ltYWdlL3BuZycsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBzaXplczogJzUxMng1MTInLFxuICAgICAgICAgIHNyYzogJ2h0dHBzOi8vdW5wa2cuY29tL0B2YmVuanMvc3RhdGljLXNvdXJjZUAwLjEuNy9zb3VyY2UvcHdhLWljb24tNTEyLnBuZycsXG4gICAgICAgICAgdHlwZTogJ2ltYWdlL3BuZycsXG4gICAgICAgIH0sXG4gICAgICBdLFxuICAgICAgaWQ6ICcvJyxcbiAgICAgIG5hbWU6ICdWYmVuIEFkbWluIERvYycsXG4gICAgICBzaG9ydF9uYW1lOiAndmJlbl9hZG1pbl9kb2MnLFxuICAgICAgdGhlbWVfY29sb3I6ICcjZmZmZmZmJyxcbiAgICB9LFxuICAgIG91dERpcjogcmVzb2x2ZShwcm9jZXNzLmN3ZCgpLCAnLnZpdGVwcmVzcy9kaXN0JyksXG4gICAgcmVnaXN0ZXJUeXBlOiAnYXV0b1VwZGF0ZScsXG4gICAgd29ya2JveDoge1xuICAgICAgZ2xvYlBhdHRlcm5zOiBbJyoqLyoue2NzcyxqcyxodG1sLHN2ZyxwbmcsaWNvLHR4dCx3b2ZmMn0nXSxcbiAgICAgIG1heGltdW1GaWxlU2l6ZVRvQ2FjaGVJbkJ5dGVzOiA1ICogMTAyNCAqIDEwMjQsXG4gICAgfSxcbiAgfTtcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL21udC9kL01pbmUvSUNQL3Z1ZS9kb2NzLy52aXRlcHJlc3MvY29uZmlnL3BsdWdpbnNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9tbnQvZC9NaW5lL0lDUC92dWUvZG9jcy8udml0ZXByZXNzL2NvbmZpZy9wbHVnaW5zL2RlbW8tcHJldmlldy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWcvcGx1Z2lucy9kZW1vLXByZXZpZXcudHNcIjtpbXBvcnQgdHlwZSB7IE1hcmtkb3duRW52LCBNYXJrZG93blJlbmRlcmVyIH0gZnJvbSAndml0ZXByZXNzJztcblxuaW1wb3J0IGNyeXB0byBmcm9tICdub2RlOmNyeXB0byc7XG5pbXBvcnQgeyByZWFkZGlyU3luYyB9IGZyb20gJ25vZGU6ZnMnO1xuaW1wb3J0IHsgam9pbiB9IGZyb20gJ25vZGU6cGF0aCc7XG5cbmV4cG9ydCBjb25zdCByYXdQYXRoUmVnZXhwID1cbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIHJlZ2V4cC9uby1zdXBlci1saW5lYXItYmFja3RyYWNraW5nLCByZWdleHAvc3RyaWN0XG4gIC9eKC4rPyg/OlxcLihbXFxkYS16XSspKT8pKCNbXFx3LV0rKT8oPzogP3soXFxkKyg/OlssLV1cXGQrKSopPyA/KFxcUyspP30pPyA/KD86XFxbKC4rKV0pPyQvO1xuXG5mdW5jdGlvbiByYXdQYXRoVG9Ub2tlbihyYXdQYXRoOiBzdHJpbmcpIHtcbiAgY29uc3QgW1xuICAgIGZpbGVwYXRoID0gJycsXG4gICAgZXh0ZW5zaW9uID0gJycsXG4gICAgcmVnaW9uID0gJycsXG4gICAgbGluZXMgPSAnJyxcbiAgICBsYW5nID0gJycsXG4gICAgcmF3VGl0bGUgPSAnJyxcbiAgXSA9IChyYXdQYXRoUmVnZXhwLmV4ZWMocmF3UGF0aCkgfHwgW10pLnNsaWNlKDEpO1xuXG4gIGNvbnN0IHRpdGxlID0gcmF3VGl0bGUgfHwgZmlsZXBhdGguc3BsaXQoJy8nKS5wb3AoKSB8fCAnJztcblxuICByZXR1cm4geyBleHRlbnNpb24sIGZpbGVwYXRoLCBsYW5nLCBsaW5lcywgcmVnaW9uLCB0aXRsZSB9O1xufVxuXG5leHBvcnQgY29uc3QgZGVtb1ByZXZpZXdQbHVnaW4gPSAobWQ6IE1hcmtkb3duUmVuZGVyZXIpID0+IHtcbiAgbWQuY29yZS5ydWxlci5hZnRlcignaW5saW5lJywgJ2RlbW8tcHJldmlldycsIChzdGF0ZSkgPT4ge1xuICAgIGNvbnN0IGluc2VydENvbXBvbmVudEltcG9ydCA9IChpbXBvcnRTdHJpbmc6IHN0cmluZykgPT4ge1xuICAgICAgY29uc3QgaW5kZXggPSBzdGF0ZS50b2tlbnMuZmluZEluZGV4KFxuICAgICAgICAoaSkgPT4gaS50eXBlID09PSAnaHRtbF9ibG9jaycgJiYgaS5jb250ZW50Lm1hdGNoKC88c2NyaXB0IHNldHVwPi9nKSxcbiAgICAgICk7XG4gICAgICBpZiAoaW5kZXggPT09IC0xKSB7XG4gICAgICAgIGNvbnN0IGltcG9ydENvbXBvbmVudCA9IG5ldyBzdGF0ZS5Ub2tlbignaHRtbF9ibG9jaycsICcnLCAwKTtcbiAgICAgICAgaW1wb3J0Q29tcG9uZW50LmNvbnRlbnQgPSBgPHNjcmlwdCBzZXR1cD5cXG4ke2ltcG9ydFN0cmluZ31cXG48L3NjcmlwdD5cXG5gO1xuICAgICAgICBzdGF0ZS50b2tlbnMuc3BsaWNlKDAsIDAsIGltcG9ydENvbXBvbmVudCk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBpZiAoc3RhdGUudG9rZW5zW2luZGV4XSkge1xuICAgICAgICAgIGNvbnN0IGNvbnRlbnQgPSBzdGF0ZS50b2tlbnNbaW5kZXhdLmNvbnRlbnQ7XG4gICAgICAgICAgc3RhdGUudG9rZW5zW2luZGV4XS5jb250ZW50ID0gY29udGVudC5yZXBsYWNlKFxuICAgICAgICAgICAgJzwvc2NyaXB0PicsXG4gICAgICAgICAgICBgJHtpbXBvcnRTdHJpbmd9XFxuPC9zY3JpcHQ+YCxcbiAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfTtcbiAgICAvLyBEZWZpbmUgdGhlIHJlZ3VsYXIgZXhwcmVzc2lvbiB0byBtYXRjaCB0aGUgZGVzaXJlZCBwYXR0ZXJuXG4gICAgY29uc3QgcmVnZXggPSAvPERlbW9QcmV2aWV3W14+XSpcXHNkaXI9XCIoW15cIl0qKVwiL2c7XG4gICAgLy8gSXRlcmF0ZSB0aHJvdWdoIHRoZSBNYXJrZG93biBjb250ZW50IGFuZCByZXBsYWNlIHRoZSBwYXR0ZXJuXG4gICAgc3RhdGUuc3JjID0gc3RhdGUuc3JjLnJlcGxhY2VBbGwocmVnZXgsIChfbWF0Y2gsIGRpcikgPT4ge1xuICAgICAgY29uc3QgY29tcG9uZW50RGlyID0gam9pbihwcm9jZXNzLmN3ZCgpLCAnc3JjJywgZGlyKS5yZXBsYWNlQWxsKFxuICAgICAgICAnXFxcXCcsXG4gICAgICAgICcvJyxcbiAgICAgICk7XG5cbiAgICAgIGxldCBjaGlsZEZpbGVzOiBzdHJpbmdbXSA9IFtdO1xuICAgICAgbGV0IGRpckV4aXN0cyA9IHRydWU7XG5cbiAgICAgIHRyeSB7XG4gICAgICAgIGNoaWxkRmlsZXMgPVxuICAgICAgICAgIHJlYWRkaXJTeW5jKGNvbXBvbmVudERpciwge1xuICAgICAgICAgICAgZW5jb2Rpbmc6ICd1dGY4JyxcbiAgICAgICAgICAgIHJlY3Vyc2l2ZTogZmFsc2UsXG4gICAgICAgICAgICB3aXRoRmlsZVR5cGVzOiBmYWxzZSxcbiAgICAgICAgICB9KSB8fCBbXTtcbiAgICAgIH0gY2F0Y2gge1xuICAgICAgICBkaXJFeGlzdHMgPSBmYWxzZTtcbiAgICAgIH1cblxuICAgICAgaWYgKCFkaXJFeGlzdHMpIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuXG4gICAgICBjb25zdCB1bmlxdWVXb3JkID0gZ2VuZXJhdGVDb250ZW50SGFzaChjb21wb25lbnREaXIpO1xuXG4gICAgICBjb25zdCBDb21wb25lbnROYW1lID0gYERlbW9Db21wb25lbnRfJHt1bmlxdWVXb3JkfWA7XG4gICAgICBpbnNlcnRDb21wb25lbnRJbXBvcnQoXG4gICAgICAgIGBpbXBvcnQgJHtDb21wb25lbnROYW1lfSBmcm9tICcke2NvbXBvbmVudERpcn0vaW5kZXgudnVlJ2AsXG4gICAgICApO1xuICAgICAgY29uc3QgeyBwYXRoOiBfcGF0aCB9ID0gc3RhdGUuZW52IGFzIE1hcmtkb3duRW52O1xuXG4gICAgICBjb25zdCBpbmRleCA9IHN0YXRlLnRva2Vucy5maW5kSW5kZXgoKGkpID0+IGkuY29udGVudC5tYXRjaChyZWdleCkpO1xuXG4gICAgICBpZiAoIXN0YXRlLnRva2Vuc1tpbmRleF0pIHtcbiAgICAgICAgcmV0dXJuICcnO1xuICAgICAgfVxuICAgICAgY29uc3QgZmlyc3RTdHJpbmcgPSAnaW5kZXgudnVlJztcbiAgICAgIGNoaWxkRmlsZXMgPSBjaGlsZEZpbGVzLnNvcnQoKGEsIGIpID0+IHtcbiAgICAgICAgaWYgKGEgPT09IGZpcnN0U3RyaW5nKSByZXR1cm4gLTE7XG4gICAgICAgIGlmIChiID09PSBmaXJzdFN0cmluZykgcmV0dXJuIDE7XG4gICAgICAgIHJldHVybiBhLmxvY2FsZUNvbXBhcmUoYiwgJ2VuJywgeyBzZW5zaXRpdml0eTogJ2Jhc2UnIH0pO1xuICAgICAgfSk7XG4gICAgICBzdGF0ZS50b2tlbnNbaW5kZXhdLmNvbnRlbnQgPVxuICAgICAgICBgPERlbW9QcmV2aWV3IGZpbGVzPVwiJHtlbmNvZGVVUklDb21wb25lbnQoSlNPTi5zdHJpbmdpZnkoY2hpbGRGaWxlcykpfVwiID48JHtDb21wb25lbnROYW1lfS8+XG4gICAgICAgIGA7XG5cbiAgICAgIGNvbnN0IF9kdW1teVRva2VuID0gbmV3IHN0YXRlLlRva2VuKCcnLCAnJywgMCk7XG4gICAgICBjb25zdCB0b2tlbkFycmF5OiBBcnJheTx0eXBlb2YgX2R1bW15VG9rZW4+ID0gW107XG4gICAgICBjaGlsZEZpbGVzLmZvckVhY2goKGZpbGVuYW1lKSA9PiB7XG4gICAgICAgIC8vIGNvbnN0IHNsb3ROYW1lID0gZmlsZW5hbWUucmVwbGFjZShleHRuYW1lKGZpbGVuYW1lKSwgJycpO1xuXG4gICAgICAgIGNvbnN0IHRlbXBsYXRlU3RhcnQgPSBuZXcgc3RhdGUuVG9rZW4oJ2h0bWxfaW5saW5lJywgJycsIDApO1xuICAgICAgICB0ZW1wbGF0ZVN0YXJ0LmNvbnRlbnQgPSBgPHRlbXBsYXRlICMke2ZpbGVuYW1lfT5gO1xuICAgICAgICB0b2tlbkFycmF5LnB1c2godGVtcGxhdGVTdGFydCk7XG5cbiAgICAgICAgY29uc3QgcmVzb2x2ZWRQYXRoID0gam9pbihjb21wb25lbnREaXIsIGZpbGVuYW1lKTtcblxuICAgICAgICBjb25zdCB7IGV4dGVuc2lvbiwgZmlsZXBhdGgsIGxhbmcsIGxpbmVzLCB0aXRsZSB9ID1cbiAgICAgICAgICByYXdQYXRoVG9Ub2tlbihyZXNvbHZlZFBhdGgpO1xuICAgICAgICAvLyBBZGQgY29kZSB0b2tlbnMgZm9yIGVhY2ggbGluZVxuICAgICAgICBjb25zdCB0b2tlbiA9IG5ldyBzdGF0ZS5Ub2tlbignZmVuY2UnLCAnY29kZScsIDApO1xuICAgICAgICB0b2tlbi5pbmZvID0gYCR7bGFuZyB8fCBleHRlbnNpb259JHtsaW5lcyA/IGB7JHtsaW5lc319YCA6ICcnfSR7XG4gICAgICAgICAgdGl0bGUgPyBgWyR7dGl0bGV9XWAgOiAnJ1xuICAgICAgICB9YDtcblxuICAgICAgICB0b2tlbi5jb250ZW50ID0gYDw8PCAke2ZpbGVwYXRofWA7XG4gICAgICAgICh0b2tlbiBhcyBhbnkpLnNyYyA9IFtyZXNvbHZlZFBhdGhdO1xuICAgICAgICB0b2tlbkFycmF5LnB1c2godG9rZW4pO1xuXG4gICAgICAgIGNvbnN0IHRlbXBsYXRlRW5kID0gbmV3IHN0YXRlLlRva2VuKCdodG1sX2lubGluZScsICcnLCAwKTtcbiAgICAgICAgdGVtcGxhdGVFbmQuY29udGVudCA9ICc8L3RlbXBsYXRlPic7XG4gICAgICAgIHRva2VuQXJyYXkucHVzaCh0ZW1wbGF0ZUVuZCk7XG4gICAgICB9KTtcbiAgICAgIGNvbnN0IGVuZFRhZyA9IG5ldyBzdGF0ZS5Ub2tlbignaHRtbF9pbmxpbmUnLCAnJywgMCk7XG4gICAgICBlbmRUYWcuY29udGVudCA9ICc8L0RlbW9QcmV2aWV3Pic7XG4gICAgICB0b2tlbkFycmF5LnB1c2goZW5kVGFnKTtcblxuICAgICAgc3RhdGUudG9rZW5zLnNwbGljZShpbmRleCArIDEsIDAsIC4uLnRva2VuQXJyYXkpO1xuXG4gICAgICAvLyBjb25zb2xlLmxvZyhcbiAgICAgIC8vICAgc3RhdGUubWQucmVuZGVyZXIucmVuZGVyKHN0YXRlLnRva2Vucywgc3RhdGU/Lm9wdGlvbnMgPz8gW10sIHN0YXRlLmVudiksXG4gICAgICAvLyApO1xuICAgICAgcmV0dXJuICcnO1xuICAgIH0pO1xuICB9KTtcbn07XG5cbmZ1bmN0aW9uIGdlbmVyYXRlQ29udGVudEhhc2goaW5wdXQ6IHN0cmluZywgbGVuZ3RoOiBudW1iZXIgPSAxMCk6IHN0cmluZyB7XG4gIC8vIFx1NEY3Rlx1NzUyOCBTSEEtMjU2IFx1NzUxRlx1NjIxMFx1NTRDOFx1NUUwQ1x1NTAzQ1xuICBjb25zdCBoYXNoID0gY3J5cHRvLmNyZWF0ZUhhc2goJ3NoYTI1NicpLnVwZGF0ZShpbnB1dCkuZGlnZXN0KCdoZXgnKTtcblxuICAvLyBcdTVDMDZcdTU0QzhcdTVFMENcdTUwM0NcdThGNkNcdTYzNjJcdTRFM0EgQmFzZTM2IFx1N0YxNlx1NzgwMVx1RkYwQ1x1NUU3Nlx1NTNENlx1NjMwN1x1NUI5QVx1OTU3Rlx1NUVBNlx1NzY4NFx1NUI1N1x1N0IyNlx1NEY1Q1x1NEUzQVx1N0VEM1x1Njc5Q1xuICByZXR1cm4gTnVtYmVyLnBhcnNlSW50KGhhc2gsIDE2KS50b1N0cmluZygzNikuc2xpY2UoMCwgbGVuZ3RoKTtcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL21udC9kL01pbmUvSUNQL3Z1ZS9kb2NzLy52aXRlcHJlc3MvY29uZmlnXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvbW50L2QvTWluZS9JQ1AvdnVlL2RvY3MvLnZpdGVwcmVzcy9jb25maWcvemgubXRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9tbnQvZC9NaW5lL0lDUC92dWUvZG9jcy8udml0ZXByZXNzL2NvbmZpZy96aC5tdHNcIjtpbXBvcnQgdHlwZSB7IERlZmF1bHRUaGVtZSB9IGZyb20gJ3ZpdGVwcmVzcyc7XG5cbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGVwcmVzcyc7XG5cbmltcG9ydCB7IHZlcnNpb24gfSBmcm9tICcuLi8uLi8uLi9wYWNrYWdlLmpzb24nO1xuXG5leHBvcnQgY29uc3QgemggPSBkZWZpbmVDb25maWcoe1xuICBkZXNjcmlwdGlvbjogJ1ZiZW4gQWRtaW4gJiBcdTRGMDFcdTRFMUFcdTdFQTdcdTdCQTFcdTc0MDZcdTdDRkJcdTdFREZcdTY4NDZcdTY3QjYnLFxuICBsYW5nOiAnemgtSGFucycsXG4gIHRoZW1lQ29uZmlnOiB7XG4gICAgZGFya01vZGVTd2l0Y2hMYWJlbDogJ1x1NEUzQlx1OTg5OCcsXG4gICAgZGFya01vZGVTd2l0Y2hUaXRsZTogJ1x1NTIwN1x1NjM2Mlx1NTIzMFx1NkRGMVx1ODI3Mlx1NkEyMVx1NUYwRicsXG4gICAgZG9jRm9vdGVyOiB7XG4gICAgICBuZXh0OiAnXHU0RTBCXHU0RTAwXHU5ODc1JyxcbiAgICAgIHByZXY6ICdcdTRFMEFcdTRFMDBcdTk4NzUnLFxuICAgIH0sXG4gICAgZWRpdExpbms6IHtcbiAgICAgIHBhdHRlcm46XG4gICAgICAgICdodHRwczovL2dpdGh1Yi5jb20vdmJlbmpzL3Z1ZS12YmVuLWFkbWluL2VkaXQvbWFpbi9kb2NzL3NyYy86cGF0aCcsXG4gICAgICB0ZXh0OiAnXHU1NzI4IEdpdEh1YiBcdTRFMEFcdTdGMTZcdThGOTFcdTZCNjRcdTk4NzVcdTk3NjInLFxuICAgIH0sXG4gICAgZm9vdGVyOiB7XG4gICAgICBjb3B5cmlnaHQ6IGBDb3B5cmlnaHQgXHUwMEE5IDIwMjAtJHtuZXcgRGF0ZSgpLmdldEZ1bGxZZWFyKCl9IFZiZW5gLFxuICAgICAgbWVzc2FnZTogJ1x1NTdGQVx1NEU4RSBNSVQgXHU4QkI4XHU1M0VGXHU1M0QxXHU1RTAzLicsXG4gICAgfSxcbiAgICBsYW5nTWVudUxhYmVsOiAnXHU1OTFBXHU4QkVEXHU4QTAwJyxcbiAgICBsYXN0VXBkYXRlZDoge1xuICAgICAgZm9ybWF0T3B0aW9uczoge1xuICAgICAgICBkYXRlU3R5bGU6ICdzaG9ydCcsXG4gICAgICAgIHRpbWVTdHlsZTogJ21lZGl1bScsXG4gICAgICB9LFxuICAgICAgdGV4dDogJ1x1NjcwMFx1NTQwRVx1NjZGNFx1NjVCMFx1NEU4RScsXG4gICAgfSxcbiAgICBsaWdodE1vZGVTd2l0Y2hUaXRsZTogJ1x1NTIwN1x1NjM2Mlx1NTIzMFx1NkQ0NVx1ODI3Mlx1NkEyMVx1NUYwRicsXG4gICAgbmF2OiBuYXYoKSxcblxuICAgIG91dGxpbmU6IHtcbiAgICAgIGxhYmVsOiAnXHU5ODc1XHU5NzYyXHU1QkZDXHU4MjJBJyxcbiAgICB9LFxuICAgIHJldHVyblRvVG9wTGFiZWw6ICdcdTU2REVcdTUyMzBcdTk4NzZcdTkwRTgnLFxuXG4gICAgc2lkZWJhcjoge1xuICAgICAgJy9jb21tZXJjaWFsLyc6IHsgYmFzZTogJy9jb21tZXJjaWFsLycsIGl0ZW1zOiBzaWRlYmFyQ29tbWVyY2lhbCgpIH0sXG4gICAgICAnL2NvbXBvbmVudHMvJzogeyBiYXNlOiAnL2NvbXBvbmVudHMvJywgaXRlbXM6IHNpZGViYXJDb21wb25lbnRzKCkgfSxcbiAgICAgICcvZ3VpZGUvJzogeyBiYXNlOiAnL2d1aWRlLycsIGl0ZW1zOiBzaWRlYmFyR3VpZGUoKSB9LFxuICAgIH0sXG4gICAgc2lkZWJhck1lbnVMYWJlbDogJ1x1ODNEQ1x1NTM1NScsXG4gIH0sXG59KTtcblxuZnVuY3Rpb24gc2lkZWJhckd1aWRlKCk6IERlZmF1bHRUaGVtZS5TaWRlYmFySXRlbVtdIHtcbiAgcmV0dXJuIFtcbiAgICB7XG4gICAgICBjb2xsYXBzZWQ6IGZhbHNlLFxuICAgICAgdGV4dDogJ1x1N0I4MFx1NEVDQicsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2ludHJvZHVjdGlvbi92YmVuJyxcbiAgICAgICAgICB0ZXh0OiAnXHU1MTczXHU0RThFIFZiZW4gQWRtaW4nLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2ludHJvZHVjdGlvbi93aHknLFxuICAgICAgICAgIHRleHQ6ICdcdTRFM0FcdTRFQzBcdTRFNDhcdTkwMDlcdTYyRTlcdTYyMTFcdTRFRUM/JyxcbiAgICAgICAgfSxcbiAgICAgICAgeyBsaW5rOiAnaW50cm9kdWN0aW9uL3F1aWNrLXN0YXJ0JywgdGV4dDogJ1x1NUZFQlx1OTAxRlx1NUYwMFx1NTlDQicgfSxcbiAgICAgICAgeyBsaW5rOiAnaW50cm9kdWN0aW9uL3RoaW4nLCB0ZXh0OiAnXHU3Q0JFXHU3QjgwXHU3MjQ4XHU2NzJDJyB9LFxuICAgICAgICB7XG4gICAgICAgICAgYmFzZTogJy8nLFxuICAgICAgICAgIGxpbms6ICdjb21wb25lbnRzL2ludHJvZHVjdGlvbicsXG4gICAgICAgICAgdGV4dDogJ1x1N0VDNFx1NEVGNlx1NjU4N1x1Njg2MycsXG4gICAgICAgIH0sXG4gICAgICBdLFxuICAgIH0sXG4gICAge1xuICAgICAgdGV4dDogJ1x1NTdGQVx1Nzg0MCcsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2NvbmNlcHQnLCB0ZXh0OiAnXHU1N0ZBXHU3ODQwXHU2OTgyXHU1RkY1JyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2RldmVsb3BtZW50JywgdGV4dDogJ1x1NjcyQ1x1NTczMFx1NUYwMFx1NTNEMScgfSxcbiAgICAgICAgeyBsaW5rOiAnZXNzZW50aWFscy9yb3V0ZScsIHRleHQ6ICdcdThERUZcdTc1MzFcdTU0OENcdTgzRENcdTUzNTUnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvc2V0dGluZ3MnLCB0ZXh0OiAnXHU5MTREXHU3RjZFJyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2ljb25zJywgdGV4dDogJ1x1NTZGRVx1NjgwNycgfSxcbiAgICAgICAgeyBsaW5rOiAnZXNzZW50aWFscy9zdHlsZXMnLCB0ZXh0OiAnXHU2ODM3XHU1RjBGJyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL2V4dGVybmFsLW1vZHVsZScsIHRleHQ6ICdcdTU5MTZcdTkwRThcdTZBMjFcdTU3NTcnIH0sXG4gICAgICAgIHsgbGluazogJ2Vzc2VudGlhbHMvYnVpbGQnLCB0ZXh0OiAnXHU2Nzg0XHU1RUZBXHU0RTBFXHU5MEU4XHU3RjcyJyB9LFxuICAgICAgICB7IGxpbms6ICdlc3NlbnRpYWxzL3NlcnZlcicsIHRleHQ6ICdcdTY3MERcdTUyQTFcdTdBRUZcdTRFQTRcdTRFOTJcdTRFMEVcdTY1NzBcdTYzNkVNb2NrJyB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIHRleHQ6ICdcdTZERjFcdTUxNjUnLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvbG9naW4nLCB0ZXh0OiAnXHU3NjdCXHU1RjU1JyB9LFxuICAgICAgICAvLyB7IGxpbms6ICdpbi1kZXB0aC9sYXlvdXQnLCB0ZXh0OiAnXHU1RTAzXHU1QzQwJyB9LFxuICAgICAgICB7IGxpbms6ICdpbi1kZXB0aC90aGVtZScsIHRleHQ6ICdcdTRFM0JcdTk4OTgnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2FjY2VzcycsIHRleHQ6ICdcdTY3NDNcdTk2NTAnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2xvY2FsZScsIHRleHQ6ICdcdTU2RkRcdTk2NDVcdTUzMTYnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2ZlYXR1cmVzJywgdGV4dDogJ1x1NUUzOFx1NzUyOFx1NTI5Rlx1ODBGRCcgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvY2hlY2stdXBkYXRlcycsIHRleHQ6ICdcdTY4QzBcdTY3RTVcdTY2RjRcdTY1QjAnIH0sXG4gICAgICAgIHsgbGluazogJ2luLWRlcHRoL2xvYWRpbmcnLCB0ZXh0OiAnXHU1MTY4XHU1QzQwbG9hZGluZycgfSxcbiAgICAgICAgeyBsaW5rOiAnaW4tZGVwdGgvdWktZnJhbWV3b3JrJywgdGV4dDogJ1x1N0VDNFx1NEVGNlx1NUU5M1x1NTIwN1x1NjM2MicgfSxcbiAgICAgIF0sXG4gICAgfSxcbiAgICB7XG4gICAgICB0ZXh0OiAnXHU1REU1XHU3QTBCJyxcbiAgICAgIGl0ZW1zOiBbXG4gICAgICAgIHsgbGluazogJ3Byb2plY3Qvc3RhbmRhcmQnLCB0ZXh0OiAnXHU4OUM0XHU4MzAzJyB9LFxuICAgICAgICB7IGxpbms6ICdwcm9qZWN0L2NsaScsIHRleHQ6ICdDTEknIH0sXG4gICAgICAgIHsgbGluazogJ3Byb2plY3QvZGlyJywgdGV4dDogJ1x1NzZFRVx1NUY1NVx1OEJGNFx1NjYwRScgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC90ZXN0JywgdGV4dDogJ1x1NTM1NVx1NTE0M1x1NkQ0Qlx1OEJENScgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC90YWlsd2luZGNzcycsIHRleHQ6ICdUYWlsd2luZCBDU1MnIH0sXG4gICAgICAgIHsgbGluazogJ3Byb2plY3QvY2hhbmdlc2V0JywgdGV4dDogJ0NoYW5nZXNldCcgfSxcbiAgICAgICAgeyBsaW5rOiAncHJvamVjdC92aXRlJywgdGV4dDogJ1ZpdGUgQ29uZmlnJyB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIHRleHQ6ICdcdTUxNzZcdTRFRDYnLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAgeyBsaW5rOiAnb3RoZXIvcHJvamVjdC11cGRhdGUnLCB0ZXh0OiAnXHU5ODc5XHU3NkVFXHU2NkY0XHU2NUIwJyB9LFxuICAgICAgICB7IGxpbms6ICdvdGhlci9yZW1vdmUtY29kZScsIHRleHQ6ICdcdTc5RkJcdTk2NjRcdTRFRTNcdTc4MDEnIH0sXG4gICAgICAgIHsgbGluazogJ290aGVyL2ZhcScsIHRleHQ6ICdcdTVFMzhcdTg5QzFcdTk1RUVcdTk4OTgnIH0sXG4gICAgICBdLFxuICAgIH0sXG4gIF07XG59XG5cbmZ1bmN0aW9uIHNpZGViYXJDb21tZXJjaWFsKCk6IERlZmF1bHRUaGVtZS5TaWRlYmFySXRlbVtdIHtcbiAgcmV0dXJuIFtcbiAgICB7XG4gICAgICBsaW5rOiAnY29tbXVuaXR5JyxcbiAgICAgIHRleHQ6ICdcdTRFQTRcdTZENDFcdTdGQTQnLFxuICAgIH0sXG4gICAge1xuICAgICAgbGluazogJ3RlY2huaWNhbC1zdXBwb3J0JyxcbiAgICAgIHRleHQ6ICdcdTYyODBcdTY3MkZcdTY1MkZcdTYzMDEnLFxuICAgIH0sXG4gICAge1xuICAgICAgbGluazogJ2N1c3RvbWl6ZWQnLFxuICAgICAgdGV4dDogJ1x1NUI5QVx1NTIzNlx1NUYwMFx1NTNEMScsXG4gICAgfSxcbiAgXTtcbn1cblxuZnVuY3Rpb24gc2lkZWJhckNvbXBvbmVudHMoKTogRGVmYXVsdFRoZW1lLlNpZGViYXJJdGVtW10ge1xuICByZXR1cm4gW1xuICAgIHtcbiAgICAgIHRleHQ6ICdcdTdFQzRcdTRFRjYnLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdpbnRyb2R1Y3Rpb24nLFxuICAgICAgICAgIHRleHQ6ICdcdTRFQ0JcdTdFQ0QnLFxuICAgICAgICB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIGNvbGxhcHNlZDogZmFsc2UsXG4gICAgICB0ZXh0OiAnXHU1RTAzXHU1QzQwXHU3RUM0XHU0RUY2JyxcbiAgICAgIGl0ZW1zOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBsaW5rOiAnbGF5b3V0LXVpL3BhZ2UnLFxuICAgICAgICAgIHRleHQ6ICdQYWdlIFx1OTg3NVx1OTc2MicsXG4gICAgICAgIH0sXG4gICAgICBdLFxuICAgIH0sXG4gICAge1xuICAgICAgY29sbGFwc2VkOiBmYWxzZSxcbiAgICAgIHRleHQ6ICdcdTkwMUFcdTc1MjhcdTdFQzRcdTRFRjYnLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdjb21tb24tdWkvdmJlbi1hcGktY29tcG9uZW50JyxcbiAgICAgICAgICB0ZXh0OiAnQXBpQ29tcG9uZW50IEFwaVx1N0VDNFx1NEVGNlx1NTMwNVx1ODhDNVx1NTY2OCcsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBsaW5rOiAnY29tbW9uLXVpL3ZiZW4tYWxlcnQnLFxuICAgICAgICAgIHRleHQ6ICdBbGVydCBcdThGN0JcdTkxQ0ZcdTYzRDBcdTc5M0FcdTY4NDYnLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2NvbW1vbi11aS92YmVuLW1vZGFsJyxcbiAgICAgICAgICB0ZXh0OiAnTW9kYWwgXHU2QTIxXHU2MDAxXHU2ODQ2JyxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdjb21tb24tdWkvdmJlbi1kcmF3ZXInLFxuICAgICAgICAgIHRleHQ6ICdEcmF3ZXIgXHU2MkJEXHU1QzQ5JyxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdjb21tb24tdWkvdmJlbi1mb3JtJyxcbiAgICAgICAgICB0ZXh0OiAnRm9ybSBcdTg4NjhcdTUzNTUnLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2NvbW1vbi11aS92YmVuLXZ4ZS10YWJsZScsXG4gICAgICAgICAgdGV4dDogJ1Z4ZSBUYWJsZSBcdTg4NjhcdTY4M0MnLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgbGluazogJ2NvbW1vbi11aS92YmVuLWNvdW50LXRvLWFuaW1hdG9yJyxcbiAgICAgICAgICB0ZXh0OiAnQ291bnRUb0FuaW1hdG9yIFx1NjU3MFx1NUI1N1x1NTJBOFx1NzUzQicsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBsaW5rOiAnY29tbW9uLXVpL3ZiZW4tZWxsaXBzaXMtdGV4dCcsXG4gICAgICAgICAgdGV4dDogJ0VsbGlwc2lzVGV4dCBcdTc3MDFcdTc1NjVcdTY1ODdcdTY3MkMnLFxuICAgICAgICB9LFxuICAgICAgXSxcbiAgICB9LFxuICBdO1xufVxuXG5mdW5jdGlvbiBuYXYoKTogRGVmYXVsdFRoZW1lLk5hdkl0ZW1bXSB7XG4gIHJldHVybiBbXG4gICAge1xuICAgICAgYWN0aXZlTWF0Y2g6ICdeLyhndWlkZXxjb21wb25lbnRzKS8nLFxuICAgICAgdGV4dDogJ1x1NjU4N1x1Njg2MycsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7XG4gICAgICAgICAgYWN0aXZlTWF0Y2g6ICdeL2d1aWRlLycsXG4gICAgICAgICAgbGluazogJy9ndWlkZS9pbnRyb2R1Y3Rpb24vdmJlbicsXG4gICAgICAgICAgdGV4dDogJ1x1NjMwN1x1NTM1NycsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBhY3RpdmVNYXRjaDogJ14vY29tcG9uZW50cy8nLFxuICAgICAgICAgIGxpbms6ICcvY29tcG9uZW50cy9pbnRyb2R1Y3Rpb24nLFxuICAgICAgICAgIHRleHQ6ICdcdTdFQzRcdTRFRjYnLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgdGV4dDogJ1x1NTM4Nlx1NTNGMlx1NzI0OFx1NjcyQycsXG4gICAgICAgICAgaXRlbXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbGluazogJ2h0dHBzOi8vZG9jLnZ2YmluLmNuJyxcbiAgICAgICAgICAgICAgdGV4dDogJzIueFx1NzI0OFx1NjcyQ1x1NjU4N1x1Njg2MycsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgIH0sXG4gICAgICBdLFxuICAgIH0sXG4gICAge1xuICAgICAgdGV4dDogJ1x1NkYxNFx1NzkzQScsXG4gICAgICBpdGVtczogW1xuICAgICAgICB7XG4gICAgICAgICAgdGV4dDogJ1ZiZW4gQWRtaW4nLFxuICAgICAgICAgIGl0ZW1zOiBbXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgIGxpbms6ICdodHRwczovL3d3dy52YmVuLnBybycsXG4gICAgICAgICAgICAgIHRleHQ6ICdcdTZGMTRcdTc5M0FcdTcyNDhcdTY3MkMnLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbGluazogJ2h0dHBzOi8vYW50LnZiZW4ucHJvJyxcbiAgICAgICAgICAgICAgdGV4dDogJ0FudCBEZXNpZ24gVnVlIFx1NzI0OFx1NjcyQycsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly9uYWl2ZS52YmVuLnBybycsXG4gICAgICAgICAgICAgIHRleHQ6ICdOYWl2ZSBcdTcyNDhcdTY3MkMnLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbGluazogJ2h0dHBzOi8vZWxlLnZiZW4ucHJvJyxcbiAgICAgICAgICAgICAgdGV4dDogJ0VsZW1lbnQgUGx1c1x1NzI0OFx1NjcyQycsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICB0ZXh0OiAnXHU1MTc2XHU0RUQ2JyxcbiAgICAgICAgICBpdGVtczogW1xuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly92YmVuLnZ2YmluLmNuJyxcbiAgICAgICAgICAgICAgdGV4dDogJ1ZiZW4gQWRtaW4gMi54JyxcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgXSxcbiAgICAgICAgfSxcbiAgICAgIF0sXG4gICAgfSxcbiAgICB7XG4gICAgICB0ZXh0OiB2ZXJzaW9uLFxuICAgICAgaXRlbXM6IFtcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdodHRwczovL2dpdGh1Yi5jb20vdmJlbmpzL3Z1ZS12YmVuLWFkbWluL3JlbGVhc2VzJyxcbiAgICAgICAgICB0ZXh0OiAnXHU2NkY0XHU2NUIwXHU2NUU1XHU1RkQ3JyxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIGxpbms6ICdodHRwczovL2dpdGh1Yi5jb20vb3Jncy92YmVuanMvcHJvamVjdHMvNScsXG4gICAgICAgICAgdGV4dDogJ1x1OERFRlx1N0VCRlx1NTZGRScsXG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBsaW5rOiAnaHR0cHM6Ly9naXRodWIuY29tL3ZiZW5qcy92dWUtdmJlbi1hZG1pbi9ibG9iL21haW4vLmdpdGh1Yi9jb250cmlidXRpbmcubWQnLFxuICAgICAgICAgIHRleHQ6ICdcdThEMjFcdTczMkUnLFxuICAgICAgICB9LFxuICAgICAgXSxcbiAgICB9LFxuICAgIHtcbiAgICAgIGxpbms6ICcvY29tbWVyY2lhbC90ZWNobmljYWwtc3VwcG9ydCcsXG4gICAgICB0ZXh0OiAnXHVEODNFXHVERDg0IFx1NjI4MFx1NjcyRlx1NjUyRlx1NjMwMScsXG4gICAgfSxcbiAgICB7XG4gICAgICBsaW5rOiAnL3Nwb25zb3IvcGVyc29uYWwnLFxuICAgICAgdGV4dDogJ1x1MjcyOCBcdThENUVcdTUyQTknLFxuICAgIH0sXG4gICAge1xuICAgICAgbGluazogJy9jb21tZXJjaWFsL2NvbW11bml0eScsXG4gICAgICB0ZXh0OiAnXHVEODNEXHVEQzY4XHUyMDBEXHVEODNEXHVEQzY2XHUyMDBEXHVEODNEXHVEQzY2IFx1NEVBNFx1NkQ0MVx1N0ZBNCcsXG4gICAgICAvLyBpdGVtczogW1xuICAgICAgLy8gICB7XG4gICAgICAvLyAgICAgbGluazogJ2h0dHBzOi8vcXVuLnFxLmNvbS9xcXdlYi9xdW5wcm8vc2hhcmU/X3d2PTMmX3d3dj0xMjgmYXBwQ2hhbm5lbD1zaGFyZSZpbnZpdGVDb2RlPTIyeVN6ajdwS2l3JmJ1c2luZXNzVHlwZT05JmZyb209MjQ2NjEwJmJpej1rYSZtYWluU291cmNlSWQ9c2hhcmUmc3ViU291cmNlSWQ9b3RoZXJzJmp1bXBzb3VyY2U9c2hvcnR1cmwjL3BjJyxcbiAgICAgIC8vICAgICB0ZXh0OiAnUVFcdTk4OTFcdTkwNTMnLFxuICAgICAgLy8gICB9LFxuICAgICAgLy8gICB7XG4gICAgICAvLyAgICAgbGluazogJ2h0dHBzOi8vcW0ucXEuY29tL2NnaS1iaW4vcW0vcXI/X3d2PTEwMjcmaz1talptbGhnVnp6VXh2ZHhsbEI2QzF2SHBYOE84UVJMMCZhdXRoS2V5PURCZEZiQndFUm1mYUtZOTVKdlJXcUxDSklSR0pBbUt5WmJycHpaNDFFS0RNWjVTUjZNZmJqT0JhYU5STjczZnImbm92ZXJpZnk9MCZncm91cF9jb2RlPTQyODYxMDknLFxuICAgICAgLy8gICAgIHRleHQ6ICdRUVx1N0ZBNCcsXG4gICAgICAvLyAgIH0sXG4gICAgICAvLyAgIHtcbiAgICAgIC8vICAgICBsaW5rOiAnaHR0cHM6Ly9kaXNjb3JkLmdnL1ZVNjJqVGVjYWQnLFxuICAgICAgLy8gICAgIHRleHQ6ICdEaXNjb3JkJyxcbiAgICAgIC8vICAgfSxcbiAgICAgIC8vIF0sXG4gICAgfSxcbiAgICAvLyB7XG4gICAgLy8gICBsaW5rOiAnL2ZyaWVuZC1saW5rcy8nLFxuICAgIC8vICAgdGV4dDogJ1x1RDgzRVx1REQxRCBcdTUzQ0JcdTYwQzVcdTk0RkVcdTYzQTUnLFxuICAgIC8vIH0sXG4gIF07XG59XG5cbmV4cG9ydCBjb25zdCBzZWFyY2g6IERlZmF1bHRUaGVtZS5BbGdvbGlhU2VhcmNoT3B0aW9uc1snbG9jYWxlcyddID0ge1xuICByb290OiB7XG4gICAgcGxhY2Vob2xkZXI6ICdcdTY0MUNcdTdEMjJcdTY1ODdcdTY4NjMnLFxuICAgIHRyYW5zbGF0aW9uczoge1xuICAgICAgYnV0dG9uOiB7XG4gICAgICAgIGJ1dHRvbkFyaWFMYWJlbDogJ1x1NjQxQ1x1N0QyMlx1NjU4N1x1Njg2MycsXG4gICAgICAgIGJ1dHRvblRleHQ6ICdcdTY0MUNcdTdEMjJcdTY1ODdcdTY4NjMnLFxuICAgICAgfSxcbiAgICAgIG1vZGFsOiB7XG4gICAgICAgIGVycm9yU2NyZWVuOiB7XG4gICAgICAgICAgaGVscFRleHQ6ICdcdTRGNjBcdTUzRUZcdTgwRkRcdTk3MDBcdTg5ODFcdTY4QzBcdTY3RTVcdTRGNjBcdTc2ODRcdTdGNTFcdTdFRENcdThGREVcdTYzQTUnLFxuICAgICAgICAgIHRpdGxlVGV4dDogJ1x1NjVFMFx1NkNENVx1ODNCN1x1NTNENlx1N0VEM1x1Njc5QycsXG4gICAgICAgIH0sXG4gICAgICAgIGZvb3Rlcjoge1xuICAgICAgICAgIGNsb3NlVGV4dDogJ1x1NTE3M1x1OTVFRCcsXG4gICAgICAgICAgbmF2aWdhdGVUZXh0OiAnXHU1MjA3XHU2MzYyJyxcbiAgICAgICAgICBzZWFyY2hCeVRleHQ6ICdcdTY0MUNcdTdEMjJcdTYzRDBcdTRGOUJcdTgwMDUnLFxuICAgICAgICAgIHNlbGVjdFRleHQ6ICdcdTkwMDlcdTYyRTknLFxuICAgICAgICB9LFxuICAgICAgICBub1Jlc3VsdHNTY3JlZW46IHtcbiAgICAgICAgICBub1Jlc3VsdHNUZXh0OiAnXHU2NUUwXHU2Q0Q1XHU2MjdFXHU1MjMwXHU3NkY4XHU1MTczXHU3RUQzXHU2NzlDJyxcbiAgICAgICAgICByZXBvcnRNaXNzaW5nUmVzdWx0c0xpbmtUZXh0OiAnXHU3MEI5XHU1MUZCXHU1M0NEXHU5OTg4JyxcbiAgICAgICAgICByZXBvcnRNaXNzaW5nUmVzdWx0c1RleHQ6ICdcdTRGNjBcdThCQTRcdTRFM0FcdThCRTVcdTY3RTVcdThCRTJcdTVFOTRcdThCRTVcdTY3MDlcdTdFRDNcdTY3OUNcdUZGMUYnLFxuICAgICAgICAgIHN1Z2dlc3RlZFF1ZXJ5VGV4dDogJ1x1NEY2MFx1NTNFRlx1NEVFNVx1NUMxRFx1OEJENVx1NjdFNVx1OEJFMicsXG4gICAgICAgIH0sXG4gICAgICAgIHNlYXJjaEJveDoge1xuICAgICAgICAgIGNhbmNlbEJ1dHRvbkFyaWFMYWJlbDogJ1x1NTNENlx1NkQ4OCcsXG4gICAgICAgICAgY2FuY2VsQnV0dG9uVGV4dDogJ1x1NTNENlx1NkQ4OCcsXG4gICAgICAgICAgcmVzZXRCdXR0b25BcmlhTGFiZWw6ICdcdTZFMDVcdTk2NjRcdTY3RTVcdThCRTJcdTY3NjFcdTRFRjYnLFxuICAgICAgICAgIHJlc2V0QnV0dG9uVGl0bGU6ICdcdTZFMDVcdTk2NjRcdTY3RTVcdThCRTJcdTY3NjFcdTRFRjYnLFxuICAgICAgICB9LFxuICAgICAgICBzdGFydFNjcmVlbjoge1xuICAgICAgICAgIGZhdm9yaXRlU2VhcmNoZXNUaXRsZTogJ1x1NjUzNlx1ODVDRicsXG4gICAgICAgICAgbm9SZWNlbnRTZWFyY2hlc1RleHQ6ICdcdTZDQTFcdTY3MDlcdTY0MUNcdTdEMjJcdTUzODZcdTUzRjInLFxuICAgICAgICAgIHJlY2VudFNlYXJjaGVzVGl0bGU6ICdcdTY0MUNcdTdEMjJcdTUzODZcdTUzRjInLFxuICAgICAgICAgIHJlbW92ZUZhdm9yaXRlU2VhcmNoQnV0dG9uVGl0bGU6ICdcdTRFQ0VcdTY1MzZcdTg1Q0ZcdTRFMkRcdTc5RkJcdTk2NjQnLFxuICAgICAgICAgIHJlbW92ZVJlY2VudFNlYXJjaEJ1dHRvblRpdGxlOiAnXHU0RUNFXHU2NDFDXHU3RDIyXHU1Mzg2XHU1M0YyXHU0RTJEXHU3OUZCXHU5NjY0JyxcbiAgICAgICAgICBzYXZlUmVjZW50U2VhcmNoQnV0dG9uVGl0bGU6ICdcdTRGRERcdTVCNThcdTgxRjNcdTY0MUNcdTdEMjJcdTUzODZcdTUzRjInLFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9LFxuICB9LFxufTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBc1MsU0FBUyxlQUFlO0FBQzlULFNBQVMsNkJBQTZCOzs7QUNDdEMsU0FBUyxvQkFBb0I7OztBQ0EzQixjQUFXOzs7QURJTixJQUFNLEtBQUssYUFBYTtBQUFBLEVBQzdCLGFBQWE7QUFBQSxFQUNiLE1BQU07QUFBQSxFQUNOLGFBQWE7QUFBQSxJQUNYLHFCQUFxQjtBQUFBLElBQ3JCLHFCQUFxQjtBQUFBLElBQ3JCLFdBQVc7QUFBQSxNQUNULE1BQU07QUFBQSxNQUNOLE1BQU07QUFBQSxJQUNSO0FBQUEsSUFDQSxVQUFVO0FBQUEsTUFDUixTQUNFO0FBQUEsTUFDRixNQUFNO0FBQUEsSUFDUjtBQUFBLElBQ0EsUUFBUTtBQUFBLE1BQ04sV0FBVyx3QkFBb0Isb0JBQUksS0FBSyxHQUFFLFlBQVksQ0FBQztBQUFBLE1BQ3ZELFNBQVM7QUFBQSxJQUNYO0FBQUEsSUFDQSxlQUFlO0FBQUEsSUFDZixhQUFhO0FBQUEsTUFDWCxlQUFlO0FBQUEsUUFDYixXQUFXO0FBQUEsUUFDWCxXQUFXO0FBQUEsTUFDYjtBQUFBLE1BQ0EsTUFBTTtBQUFBLElBQ1I7QUFBQSxJQUNBLHNCQUFzQjtBQUFBLElBQ3RCLEtBQUssSUFBSTtBQUFBLElBQ1QsU0FBUztBQUFBLE1B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QUFPO0FBQUEsSUFDTDtBQUFBLE1BQ0UsTUFBTTtBQUFBLE1BQ04sTUFBTTtBQUFBLElBQ1I7QUFBQSxJQUNBO0FBQUEsTUFDRSxNQUFNO0FBQUEsTUFDTixNQUFNO0FBQUEsSUFDUjtBQUFBLElBQ0E7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLE1BQU07QUFBQSxJQUNSO0FBQUEsRUFDRjtBQUNGO0FBRUEsU0FBUyxNQUE4QjtBQUNyQyxTQUFPO0FBQUEsSUFDTDtBQUFBLE1BQ0UsYUFBYTtBQUFBLE1BQ2IsTUFBTTtBQUFBLE1BQ04sT0FBTztBQUFBLFFBQ0w7QUFBQSxVQUNFLGFBQWE7QUFBQSxVQUNiLE1BQU07QUFBQSxVQUNOLE1BQU07QUFBQSxRQUNSO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBLFFBTUE7QUFBQSxVQUNFLE1BQU07QUFBQSxVQUNOLE9BQU87QUFBQSxZQUNMO0FBQUEsY0FDRSxNQUFNO0FBQUEsY0FDTixNQUFNO0FBQUEsWUFDUjtBQUFBLFVBQ0Y7QUFBQSxRQUNGO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxJQUNBO0FBQUEsTUFDRSxNQUFNO0FBQUEsTUFDTixPQUFPO0FBQUEsUUFDTDtBQUFBLFVBQ0UsTUFBTTtBQUFBLFVBQ04sT0FBTztBQUFBLFlBQ0w7QUFBQSxjQUNFLE1BQU07QUFBQSxjQUNOLE1BQU07QUFBQSxZQUNSO0FBQUEsWUFDQTtBQUFBLGNBQ0UsTUFBTTtBQUFBLGNBQ04sTUFBTTtBQUFBLFlBQ1I7QUFBQSxZQUNBO0FBQUEsY0FDRSxNQUFNO0FBQUEsY0FDTixNQUFNO0FBQUEsWUFDUjtBQUFBLFlBQ0E7QUFBQSxjQUNFLE1BQU07QUFBQSxjQUNOLE1BQU07QUFBQSxZQUNSO0FBQUEsVUFDRjtBQUFBLFFBQ0Y7QUFBQSxRQUNBO0FBQUEsVUFDRSxNQUFNO0FBQUEsVUFDTixPQUFPO0FBQUEsWUFDTDtBQUFBLGNBQ0UsTUFBTTtBQUFBLGNBQ04sTUFBTTtBQUFBLFlBQ1I7QUFBQSxVQUNGO0FBQUEsUUFDRjtBQUFBLE1BQ0Y7QUFBQSxJQUNG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QUNSO0FBQUEsUUFDQTtBQUFBLFVBQ0UsTUFBTTtBQUFBLFVBQ04sTUFBTTtBQUFBLFFBQ1I7QUFBQSxRQUNBO0FBQUEsVUFDRSxNQUFNO0FBQUEsVUFDTixNQUFNO0FBQUEsUUFDUjtBQUFBLFFBQ0E7QUFBQSxVQUNFLE1BQU07QUFBQSxVQUNOLE1BQU07QUFBQSxRQUNSO0FBQUEsUUFDQTtBQUFBLFVBQ0UsTUFBTTtBQUFBLFVBQ04sTUFBTTtBQUFBLFFBQ1I7QUFBQSxRQUNBO0FBQUEsVUFDRSxNQUFNO0FBQUEsVUFDTixNQUFNO0FBQUEsUUFDUjtBQUFBLFFBQ0E7QUFBQSxVQUNFLE1BQU07QUFBQSxVQUNOLE1BQU07QUFBQSxRQUNSO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0Y7QUFFQSxTQUFTRCxPQUE4QjtBQUNyQyxTQUFPO0FBQUEsSUFDTDtBQUFBLE1BQ0UsYUFBYTtBQUFBLE1BQ2IsTUFBTTtBQUFBLE1BQ04sT0FBTztBQUFBLFFBQ0w7QUFBQSxVQUNFLGFBQWE7QUFBQSxVQUNiLE1BQU07QUFBQSxVQUNOLE1BQU07QUFBQSxRQUNSO0FBQUEsUUFDQTtBQUFBLFVBQ0UsYUFBYTtBQUFBLFVBQ2IsTUFBTTtBQUFBLFVBQ04sTUFBTTtBQUFBLFFBQ1I7QUFBQSxRQUNBO0FBQUEsVUFDRSxNQUFNO0FBQUEsVUFDTixPQUFPO0FBQUEsWUFDTDtBQUFBLGNBQ0UsTUFBTTtBQUFBLGNBQ04sTUFBTTtBQUFBLFlBQ1I7QUFBQSxVQUNGO0FBQUEsUUFDRjtBQUFBLE1BQ0Y7QUFBQSxJQUNGO0FBQUEsSUFDQTtBQUFBLE1BQ0UsTUFBTTtBQUFBLE1BQ04sT0FBTztBQUFBLFFBQ0w7QUFBQSxVQUNFLE1BQU07QUFBQSxVQUNOLE9BQU87QUFBQSxZQUNMO0FBQUEsY0FDRSxNQUFNO0FBQUEsY0FDTixNQUFNO0FBQUEsWUFDUjtBQUFBLFlBQ0E7QUFBQSxjQUNFLE1BQU07QUFBQSxjQUNOLE1BQU07QUFBQSxZQUNSO0FBQUEsWUFDQTtBQUFBLGNBQ0UsTUFBTTtBQUFBLGNBQ04sTUFB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LFNBQVMsQ0FBQztBQUFBLElBQ25EO0FBQUEsTUFDRTtBQUFBLE1BQ0E7QUFBQSxRQUNFLFNBQVM7QUFBQSxRQUNULE1BQU07QUFBQSxNQUNSO0FBQUEsSUFDRjtBQUFBLElBQ0EsQ0FBQyxRQUFRLEVBQUUsTUFBTSxnQkFBZ0IsS0FBSyxRQUFRLE1BQU0sZ0JBQWdCLENBQUM7QUFBQSxJQUNyRTtBQUFBLE1BQ0U7QUFBQSxNQUNBO0FBQUEsUUFDRSxTQUNFO0FBQUEsUUFDRixNQUFNO0FBQUEsTUFDUjtBQUFBLElBQ0Y7QUFBQSxJQUNBLENBQUMsUUFBUSxFQUFFLFNBQVMsbUJBQW1CLE1BQU0sV0FBVyxDQUFDO0FBQUEsSUFDekQsQ0FBQyxRQUFRLEVBQUUsTUFBTSxnQkFBZ0IsS0FBSyxPQUFPLENBQUM7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQSxFQU9oRDtBQUNGO0FBRUEsU0FBUyxNQUFrQjtBQUN6QixTQUFPO0FBQUEsSUFDTCxzQkFBc0I7QUFBQSxJQUN0QixVQUFVO0FBQUEsTUFDUixhQUNFO0FBQUEsTUFDRixPQUFPO0FBQUEsUUFDTDtBQUFBLFVBQ0UsT0FBTztBQUFBLFVBQ1AsS0FBSztBQUFBLFVBQ0wsTUFBTTtBQUFBLFFBQ1I7QUFBQSxRQUNBO0FBQUEsVUFDRSxPQUFPO0FBQUEsVUFDUCxLQUFLO0FBQUEsVUFDTCxNQUFNO0FBQUEsUUFDUjtBQUFBLE1BQ0Y7QUFBQSxNQUNBLElBQUk7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFlBQVk7QUFBQSxNQUNaLGFBQWE7QUFBQSxJQUNmO0FBQUEsSUFDQSxRQUFRLFFBQVEsUUFBUSxJQUFJLEdBQUcsaUJBQWlCO0FBQUEsSUFDaEQsY0FBYztBQUFBLElBQ2QsU0FBUztBQUFBLE1BQ1AsY0FBYyxDQUFDLDBDQUEwQztBQUFBLE1BQ3pELCtCQUErQixJQUFJLE9BQU87QUFBQSxJQUM1QztBQUFBLEVBQ0Y7QUFDRjs7O0FIcEtBLElBQU8sZ0JBQVE7QUFBQSxFQUNiLHNCQUFzQjtBQUFBLElBQ3BCLEdBQUc7QUFBQSxJQUNILFNBQVM7QUFBQSxNQUNQLElBQUk7QUFBQSxRQUNGLE9BQU87QUFBQSxRQUNQLE1BQU07QUFBQSxRQUNOLE1BQU07QUFBQSxRQUNOLEdBQUc7QUFBQSxNQUNMO0FBQUEsTUFDQSxNQUFNO0FBQUEsUUFDSixPQUFPO0FBQUEsUUFDUCxNQUFNO0FBQUEsUUFDTixHQUFHO0FBQUEsTUFDTDtBQUFBLElBQ0Y7QUFBQSxFQUNGLENBQUM7QUFDSDsiLAogICJuYW1lcyI6IFsiZGVmaW5lQ29uZmlnIiwgImRlZmluZUNvbmZpZyIsICJkZWZpbmVDb25maWciLCAibmF2IiwgInNpZGViYXJDb21tZXJjaWFsIiwgInNpZGViYXJHdWlkZSIsICJkZWZpbmVDb25maWciXQp9Cg==