Commit 626352186da30dab810b1e319b83a7991d3166b8
0 parents
Initial commit: xlyAi ERP AI service (Spring Boot + LangChain4j)
Showing
144 changed files
with
20523 additions
and
0 deletions
Too many changes to show.
To preserve performance only 68 of 144 files are displayed.
.gitignore
0 → 100644
| 1 | +++ a/.gitignore | ||
| 1 | +HELP.md | ||
| 2 | +target/ | ||
| 3 | +.mvn/wrapper/maven-wrapper.jar | ||
| 4 | +!**/src/main/**/target/ | ||
| 5 | +!**/src/test/**/target/ | ||
| 6 | + | ||
| 7 | +### STS ### | ||
| 8 | +.apt_generated | ||
| 9 | +.classpath | ||
| 10 | +.factorypath | ||
| 11 | +.project | ||
| 12 | +.settings | ||
| 13 | +.springBeans | ||
| 14 | +.sts4-cache | ||
| 15 | + | ||
| 16 | +### IntelliJ IDEA ### | ||
| 17 | +.idea | ||
| 18 | +*.iws | ||
| 19 | +*.iml | ||
| 20 | +*.ipr | ||
| 21 | + | ||
| 22 | +### NetBeans ### | ||
| 23 | +/nbproject/private/ | ||
| 24 | +/nbbuild/ | ||
| 25 | +/dist/ | ||
| 26 | +/nbdist/ | ||
| 27 | +/.nb-gradle/ | ||
| 28 | +build/ | ||
| 29 | +!**/src/main/**/build/ | ||
| 30 | +!**/src/test/**/build/ | ||
| 31 | + | ||
| 32 | +### VS Code ### | ||
| 33 | +.vscode/ |
.mvn/wrapper/maven-wrapper.properties
0 → 100644
mvnw
0 → 100644
| 1 | +++ a/mvnw | ||
| 1 | +#!/bin/sh | ||
| 2 | +# ---------------------------------------------------------------------------- | ||
| 3 | +# Licensed to the Apache Software Foundation (ASF) under one | ||
| 4 | +# or more contributor license agreements. See the NOTICE file | ||
| 5 | +# distributed with this work for additional information | ||
| 6 | +# regarding copyright ownership. The ASF licenses this file | ||
| 7 | +# to you under the Apache License, Version 2.0 (the | ||
| 8 | +# "License"); you may not use this file except in compliance | ||
| 9 | +# with the License. You may obtain a copy of the License at | ||
| 10 | +# | ||
| 11 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
| 12 | +# | ||
| 13 | +# Unless required by applicable law or agreed to in writing, | ||
| 14 | +# software distributed under the License is distributed on an | ||
| 15 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| 16 | +# KIND, either express or implied. See the License for the | ||
| 17 | +# specific language governing permissions and limitations | ||
| 18 | +# under the License. | ||
| 19 | +# ---------------------------------------------------------------------------- | ||
| 20 | + | ||
| 21 | +# ---------------------------------------------------------------------------- | ||
| 22 | +# Apache Maven Wrapper startup batch script, version 3.3.4 | ||
| 23 | +# | ||
| 24 | +# Optional ENV vars | ||
| 25 | +# ----------------- | ||
| 26 | +# JAVA_HOME - location of a JDK home dir, required when download maven via java source | ||
| 27 | +# MVNW_REPOURL - repo url base for downloading maven distribution | ||
| 28 | +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven | ||
| 29 | +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output | ||
| 30 | +# ---------------------------------------------------------------------------- | ||
| 31 | + | ||
| 32 | +set -euf | ||
| 33 | +[ "${MVNW_VERBOSE-}" != debug ] || set -x | ||
| 34 | + | ||
| 35 | +# OS specific support. | ||
| 36 | +native_path() { printf %s\\n "$1"; } | ||
| 37 | +case "$(uname)" in | ||
| 38 | +CYGWIN* | MINGW*) | ||
| 39 | + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" | ||
| 40 | + native_path() { cygpath --path --windows "$1"; } | ||
| 41 | + ;; | ||
| 42 | +esac | ||
| 43 | + | ||
| 44 | +# set JAVACMD and JAVACCMD | ||
| 45 | +set_java_home() { | ||
| 46 | + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched | ||
| 47 | + if [ -n "${JAVA_HOME-}" ]; then | ||
| 48 | + if [ -x "$JAVA_HOME/jre/sh/java" ]; then | ||
| 49 | + # IBM's JDK on AIX uses strange locations for the executables | ||
| 50 | + JAVACMD="$JAVA_HOME/jre/sh/java" | ||
| 51 | + JAVACCMD="$JAVA_HOME/jre/sh/javac" | ||
| 52 | + else | ||
| 53 | + JAVACMD="$JAVA_HOME/bin/java" | ||
| 54 | + JAVACCMD="$JAVA_HOME/bin/javac" | ||
| 55 | + | ||
| 56 | + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then | ||
| 57 | + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 | ||
| 58 | + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 | ||
| 59 | + return 1 | ||
| 60 | + fi | ||
| 61 | + fi | ||
| 62 | + else | ||
| 63 | + JAVACMD="$( | ||
| 64 | + 'set' +e | ||
| 65 | + 'unset' -f command 2>/dev/null | ||
| 66 | + 'command' -v java | ||
| 67 | + )" || : | ||
| 68 | + JAVACCMD="$( | ||
| 69 | + 'set' +e | ||
| 70 | + 'unset' -f command 2>/dev/null | ||
| 71 | + 'command' -v javac | ||
| 72 | + )" || : | ||
| 73 | + | ||
| 74 | + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then | ||
| 75 | + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 | ||
| 76 | + return 1 | ||
| 77 | + fi | ||
| 78 | + fi | ||
| 79 | +} | ||
| 80 | + | ||
| 81 | +# hash string like Java String::hashCode | ||
| 82 | +hash_string() { | ||
| 83 | + str="${1:-}" h=0 | ||
| 84 | + while [ -n "$str" ]; do | ||
| 85 | + char="${str%"${str#?}"}" | ||
| 86 | + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) | ||
| 87 | + str="${str#?}" | ||
| 88 | + done | ||
| 89 | + printf %x\\n $h | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +verbose() { :; } | ||
| 93 | +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } | ||
| 94 | + | ||
| 95 | +die() { | ||
| 96 | + printf %s\\n "$1" >&2 | ||
| 97 | + exit 1 | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +trim() { | ||
| 101 | + # MWRAPPER-139: | ||
| 102 | + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. | ||
| 103 | + # Needed for removing poorly interpreted newline sequences when running in more | ||
| 104 | + # exotic environments such as mingw bash on Windows. | ||
| 105 | + printf "%s" "${1}" | tr -d '[:space:]' | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +scriptDir="$(dirname "$0")" | ||
| 109 | +scriptName="$(basename "$0")" | ||
| 110 | + | ||
| 111 | +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties | ||
| 112 | +while IFS="=" read -r key value; do | ||
| 113 | + case "${key-}" in | ||
| 114 | + distributionUrl) distributionUrl=$(trim "${value-}") ;; | ||
| 115 | + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; | ||
| 116 | + esac | ||
| 117 | +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ||
| 118 | +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" | ||
| 119 | + | ||
| 120 | +case "${distributionUrl##*/}" in | ||
| 121 | +maven-mvnd-*bin.*) | ||
| 122 | + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ | ||
| 123 | + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in | ||
| 124 | + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; | ||
| 125 | + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; | ||
| 126 | + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; | ||
| 127 | + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; | ||
| 128 | + *) | ||
| 129 | + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 | ||
| 130 | + distributionPlatform=linux-amd64 | ||
| 131 | + ;; | ||
| 132 | + esac | ||
| 133 | + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" | ||
| 134 | + ;; | ||
| 135 | +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; | ||
| 136 | +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; | ||
| 137 | +esac | ||
| 138 | + | ||
| 139 | +# apply MVNW_REPOURL and calculate MAVEN_HOME | ||
| 140 | +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> | ||
| 141 | +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" | ||
| 142 | +distributionUrlName="${distributionUrl##*/}" | ||
| 143 | +distributionUrlNameMain="${distributionUrlName%.*}" | ||
| 144 | +distributionUrlNameMain="${distributionUrlNameMain%-bin}" | ||
| 145 | +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" | ||
| 146 | +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" | ||
| 147 | + | ||
| 148 | +exec_maven() { | ||
| 149 | + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : | ||
| 150 | + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +if [ -d "$MAVEN_HOME" ]; then | ||
| 154 | + verbose "found existing MAVEN_HOME at $MAVEN_HOME" | ||
| 155 | + exec_maven "$@" | ||
| 156 | +fi | ||
| 157 | + | ||
| 158 | +case "${distributionUrl-}" in | ||
| 159 | +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; | ||
| 160 | +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; | ||
| 161 | +esac | ||
| 162 | + | ||
| 163 | +# prepare tmp dir | ||
| 164 | +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then | ||
| 165 | + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } | ||
| 166 | + trap clean HUP INT TERM EXIT | ||
| 167 | +else | ||
| 168 | + die "cannot create temp dir" | ||
| 169 | +fi | ||
| 170 | + | ||
| 171 | +mkdir -p -- "${MAVEN_HOME%/*}" | ||
| 172 | + | ||
| 173 | +# Download and Install Apache Maven | ||
| 174 | +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." | ||
| 175 | +verbose "Downloading from: $distributionUrl" | ||
| 176 | +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" | ||
| 177 | + | ||
| 178 | +# select .zip or .tar.gz | ||
| 179 | +if ! command -v unzip >/dev/null; then | ||
| 180 | + distributionUrl="${distributionUrl%.zip}.tar.gz" | ||
| 181 | + distributionUrlName="${distributionUrl##*/}" | ||
| 182 | +fi | ||
| 183 | + | ||
| 184 | +# verbose opt | ||
| 185 | +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' | ||
| 186 | +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v | ||
| 187 | + | ||
| 188 | +# normalize http auth | ||
| 189 | +case "${MVNW_PASSWORD:+has-password}" in | ||
| 190 | +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; | ||
| 191 | +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; | ||
| 192 | +esac | ||
| 193 | + | ||
| 194 | +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then | ||
| 195 | + verbose "Found wget ... using wget" | ||
| 196 | + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" | ||
| 197 | +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then | ||
| 198 | + verbose "Found curl ... using curl" | ||
| 199 | + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" | ||
| 200 | +elif set_java_home; then | ||
| 201 | + verbose "Falling back to use Java to download" | ||
| 202 | + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" | ||
| 203 | + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" | ||
| 204 | + cat >"$javaSource" <<-END | ||
| 205 | + public class Downloader extends java.net.Authenticator | ||
| 206 | + { | ||
| 207 | + protected java.net.PasswordAuthentication getPasswordAuthentication() | ||
| 208 | + { | ||
| 209 | + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); | ||
| 210 | + } | ||
| 211 | + public static void main( String[] args ) throws Exception | ||
| 212 | + { | ||
| 213 | + setDefault( new Downloader() ); | ||
| 214 | + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); | ||
| 215 | + } | ||
| 216 | + } | ||
| 217 | + END | ||
| 218 | + # For Cygwin/MinGW, switch paths to Windows format before running javac and java | ||
| 219 | + verbose " - Compiling Downloader.java ..." | ||
| 220 | + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" | ||
| 221 | + verbose " - Running Downloader.java ..." | ||
| 222 | + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" | ||
| 223 | +fi | ||
| 224 | + | ||
| 225 | +# If specified, validate the SHA-256 sum of the Maven distribution zip file | ||
| 226 | +if [ -n "${distributionSha256Sum-}" ]; then | ||
| 227 | + distributionSha256Result=false | ||
| 228 | + if [ "$MVN_CMD" = mvnd.sh ]; then | ||
| 229 | + echo "Checksum validation is not supported for maven-mvnd." >&2 | ||
| 230 | + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 | ||
| 231 | + exit 1 | ||
| 232 | + elif command -v sha256sum >/dev/null; then | ||
| 233 | + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then | ||
| 234 | + distributionSha256Result=true | ||
| 235 | + fi | ||
| 236 | + elif command -v shasum >/dev/null; then | ||
| 237 | + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then | ||
| 238 | + distributionSha256Result=true | ||
| 239 | + fi | ||
| 240 | + else | ||
| 241 | + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 | ||
| 242 | + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 | ||
| 243 | + exit 1 | ||
| 244 | + fi | ||
| 245 | + if [ $distributionSha256Result = false ]; then | ||
| 246 | + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 | ||
| 247 | + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 | ||
| 248 | + exit 1 | ||
| 249 | + fi | ||
| 250 | +fi | ||
| 251 | + | ||
| 252 | +# unzip and move | ||
| 253 | +if command -v unzip >/dev/null; then | ||
| 254 | + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" | ||
| 255 | +else | ||
| 256 | + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" | ||
| 257 | +fi | ||
| 258 | + | ||
| 259 | +# Find the actual extracted directory name (handles snapshots where filename != directory name) | ||
| 260 | +actualDistributionDir="" | ||
| 261 | + | ||
| 262 | +# First try the expected directory name (for regular distributions) | ||
| 263 | +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then | ||
| 264 | + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then | ||
| 265 | + actualDistributionDir="$distributionUrlNameMain" | ||
| 266 | + fi | ||
| 267 | +fi | ||
| 268 | + | ||
| 269 | +# If not found, search for any directory with the Maven executable (for snapshots) | ||
| 270 | +if [ -z "$actualDistributionDir" ]; then | ||
| 271 | + # enable globbing to iterate over items | ||
| 272 | + set +f | ||
| 273 | + for dir in "$TMP_DOWNLOAD_DIR"/*; do | ||
| 274 | + if [ -d "$dir" ]; then | ||
| 275 | + if [ -f "$dir/bin/$MVN_CMD" ]; then | ||
| 276 | + actualDistributionDir="$(basename "$dir")" | ||
| 277 | + break | ||
| 278 | + fi | ||
| 279 | + fi | ||
| 280 | + done | ||
| 281 | + set -f | ||
| 282 | +fi | ||
| 283 | + | ||
| 284 | +if [ -z "$actualDistributionDir" ]; then | ||
| 285 | + verbose "Contents of $TMP_DOWNLOAD_DIR:" | ||
| 286 | + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" | ||
| 287 | + die "Could not find Maven distribution directory in extracted archive" | ||
| 288 | +fi | ||
| 289 | + | ||
| 290 | +verbose "Found extracted Maven distribution directory: $actualDistributionDir" | ||
| 291 | +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" | ||
| 292 | +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" | ||
| 293 | + | ||
| 294 | +clean || : | ||
| 295 | +exec_maven "$@" |
mvnw.cmd
0 → 100644
| 1 | +++ a/mvnw.cmd | ||
| 1 | +<# : batch portion | ||
| 2 | +@REM ---------------------------------------------------------------------------- | ||
| 3 | +@REM Licensed to the Apache Software Foundation (ASF) under one | ||
| 4 | +@REM or more contributor license agreements. See the NOTICE file | ||
| 5 | +@REM distributed with this work for additional information | ||
| 6 | +@REM regarding copyright ownership. The ASF licenses this file | ||
| 7 | +@REM to you under the Apache License, Version 2.0 (the | ||
| 8 | +@REM "License"); you may not use this file except in compliance | ||
| 9 | +@REM with the License. You may obtain a copy of the License at | ||
| 10 | +@REM | ||
| 11 | +@REM http://www.apache.org/licenses/LICENSE-2.0 | ||
| 12 | +@REM | ||
| 13 | +@REM Unless required by applicable law or agreed to in writing, | ||
| 14 | +@REM software distributed under the License is distributed on an | ||
| 15 | +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| 16 | +@REM KIND, either express or implied. See the License for the | ||
| 17 | +@REM specific language governing permissions and limitations | ||
| 18 | +@REM under the License. | ||
| 19 | +@REM ---------------------------------------------------------------------------- | ||
| 20 | + | ||
| 21 | +@REM ---------------------------------------------------------------------------- | ||
| 22 | +@REM Apache Maven Wrapper startup batch script, version 3.3.4 | ||
| 23 | +@REM | ||
| 24 | +@REM Optional ENV vars | ||
| 25 | +@REM MVNW_REPOURL - repo url base for downloading maven distribution | ||
| 26 | +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven | ||
| 27 | +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output | ||
| 28 | +@REM ---------------------------------------------------------------------------- | ||
| 29 | + | ||
| 30 | +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) | ||
| 31 | +@SET __MVNW_CMD__= | ||
| 32 | +@SET __MVNW_ERROR__= | ||
| 33 | +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% | ||
| 34 | +@SET PSModulePath= | ||
| 35 | +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( | ||
| 36 | + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) | ||
| 37 | +) | ||
| 38 | +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% | ||
| 39 | +@SET __MVNW_PSMODULEP_SAVE= | ||
| 40 | +@SET __MVNW_ARG0_NAME__= | ||
| 41 | +@SET MVNW_USERNAME= | ||
| 42 | +@SET MVNW_PASSWORD= | ||
| 43 | +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) | ||
| 44 | +@echo Cannot start maven from wrapper >&2 && exit /b 1 | ||
| 45 | +@GOTO :EOF | ||
| 46 | +: end batch / begin powershell #> | ||
| 47 | + | ||
| 48 | +$ErrorActionPreference = "Stop" | ||
| 49 | +if ($env:MVNW_VERBOSE -eq "true") { | ||
| 50 | + $VerbosePreference = "Continue" | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties | ||
| 54 | +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl | ||
| 55 | +if (!$distributionUrl) { | ||
| 56 | + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { | ||
| 60 | + "maven-mvnd-*" { | ||
| 61 | + $USE_MVND = $true | ||
| 62 | + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" | ||
| 63 | + $MVN_CMD = "mvnd.cmd" | ||
| 64 | + break | ||
| 65 | + } | ||
| 66 | + default { | ||
| 67 | + $USE_MVND = $false | ||
| 68 | + $MVN_CMD = $script -replace '^mvnw','mvn' | ||
| 69 | + break | ||
| 70 | + } | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +# apply MVNW_REPOURL and calculate MAVEN_HOME | ||
| 74 | +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> | ||
| 75 | +if ($env:MVNW_REPOURL) { | ||
| 76 | + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } | ||
| 77 | + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" | ||
| 78 | +} | ||
| 79 | +$distributionUrlName = $distributionUrl -replace '^.*/','' | ||
| 80 | +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' | ||
| 81 | + | ||
| 82 | +$MAVEN_M2_PATH = "$HOME/.m2" | ||
| 83 | +if ($env:MAVEN_USER_HOME) { | ||
| 84 | + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { | ||
| 88 | + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +$MAVEN_WRAPPER_DISTS = $null | ||
| 92 | +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { | ||
| 93 | + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" | ||
| 94 | +} else { | ||
| 95 | + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" | ||
| 99 | +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' | ||
| 100 | +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" | ||
| 101 | + | ||
| 102 | +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { | ||
| 103 | + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" | ||
| 104 | + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" | ||
| 105 | + exit $? | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { | ||
| 109 | + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" | ||
| 110 | +} | ||
| 111 | + | ||
| 112 | +# prepare tmp dir | ||
| 113 | +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile | ||
| 114 | +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" | ||
| 115 | +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null | ||
| 116 | +trap { | ||
| 117 | + if ($TMP_DOWNLOAD_DIR.Exists) { | ||
| 118 | + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } | ||
| 119 | + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } | ||
| 120 | + } | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null | ||
| 124 | + | ||
| 125 | +# Download and Install Apache Maven | ||
| 126 | +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." | ||
| 127 | +Write-Verbose "Downloading from: $distributionUrl" | ||
| 128 | +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" | ||
| 129 | + | ||
| 130 | +$webclient = New-Object System.Net.WebClient | ||
| 131 | +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { | ||
| 132 | + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) | ||
| 133 | +} | ||
| 134 | +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||
| 135 | +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null | ||
| 136 | + | ||
| 137 | +# If specified, validate the SHA-256 sum of the Maven distribution zip file | ||
| 138 | +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum | ||
| 139 | +if ($distributionSha256Sum) { | ||
| 140 | + if ($USE_MVND) { | ||
| 141 | + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." | ||
| 142 | + } | ||
| 143 | + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash | ||
| 144 | + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { | ||
| 145 | + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." | ||
| 146 | + } | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +# unzip and move | ||
| 150 | +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null | ||
| 151 | + | ||
| 152 | +# Find the actual extracted directory name (handles snapshots where filename != directory name) | ||
| 153 | +$actualDistributionDir = "" | ||
| 154 | + | ||
| 155 | +# First try the expected directory name (for regular distributions) | ||
| 156 | +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" | ||
| 157 | +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" | ||
| 158 | +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { | ||
| 159 | + $actualDistributionDir = $distributionUrlNameMain | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +# If not found, search for any directory with the Maven executable (for snapshots) | ||
| 163 | +if (!$actualDistributionDir) { | ||
| 164 | + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { | ||
| 165 | + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" | ||
| 166 | + if (Test-Path -Path $testPath -PathType Leaf) { | ||
| 167 | + $actualDistributionDir = $_.Name | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +if (!$actualDistributionDir) { | ||
| 173 | + Write-Error "Could not find Maven distribution directory in extracted archive" | ||
| 174 | +} | ||
| 175 | + | ||
| 176 | +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" | ||
| 177 | +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null | ||
| 178 | +try { | ||
| 179 | + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null | ||
| 180 | +} catch { | ||
| 181 | + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { | ||
| 182 | + Write-Error "fail to move MAVEN_HOME" | ||
| 183 | + } | ||
| 184 | +} finally { | ||
| 185 | + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } | ||
| 186 | + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } | ||
| 187 | +} | ||
| 188 | + | ||
| 189 | +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" |
pom.xml
0 → 100644
| 1 | +++ a/pom.xml | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| 3 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 4 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 | ||
| 5 | + http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| 6 | + <modelVersion>4.0.0</modelVersion> | ||
| 7 | + | ||
| 8 | + <parent> | ||
| 9 | + <groupId>org.springframework.boot</groupId> | ||
| 10 | + <artifactId>spring-boot-starter-parent</artifactId> | ||
| 11 | + <version>3.5.9</version> | ||
| 12 | + <relativePath/> | ||
| 13 | + </parent> | ||
| 14 | + | ||
| 15 | + <groupId>com.xly</groupId> | ||
| 16 | + <artifactId>xlyAi</artifactId> | ||
| 17 | + <version>0.0.1-SNAPSHOT</version> | ||
| 18 | + <packaging>war</packaging> | ||
| 19 | + <name>xlyAi</name> | ||
| 20 | + <description>xlyAi</description> | ||
| 21 | + | ||
| 22 | + <properties> | ||
| 23 | + <java.version>17</java.version> | ||
| 24 | + <mybatis-spring.version>3.0.4</mybatis-spring.version> | ||
| 25 | + <pagehelper.version>2.1.0</pagehelper.version> | ||
| 26 | + <mapstruct.version>1.5.5.Final</mapstruct.version> | ||
| 27 | + <fastjson.version>2.0.50</fastjson.version> | ||
| 28 | + <langchain4j.version>1.14.0</langchain4j.version> | ||
| 29 | + <langchain4j-emb.version>1.14.0-beta24</langchain4j-emb.version> | ||
| 30 | + <lombok.version>1.18.34</lombok.version> | ||
| 31 | + <hutool.version>5.8.28</hutool.version> | ||
| 32 | + <okhttp.version>4.10.0</okhttp.version> | ||
| 33 | + <springdoc.version>2.5.0</springdoc.version> | ||
| 34 | + <jackson.version>2.17.2</jackson.version> | ||
| 35 | + <milvus.version>2.6.15</milvus.version> | ||
| 36 | + <tess4j.version>5.18.0</tess4j.version> | ||
| 37 | + <jakarta-validation.version>3.1.0</jakarta-validation.version> | ||
| 38 | + </properties> | ||
| 39 | + | ||
| 40 | + <dependencies> | ||
| 41 | + <!-- Spring Boot --> | ||
| 42 | + <dependency> | ||
| 43 | + <groupId>org.springframework.boot</groupId> | ||
| 44 | + <artifactId>spring-boot-starter-web</artifactId> | ||
| 45 | + </dependency> | ||
| 46 | + <dependency> | ||
| 47 | + <groupId>org.springframework.boot</groupId> | ||
| 48 | + <artifactId>spring-boot-starter-actuator</artifactId> | ||
| 49 | + </dependency> | ||
| 50 | + <dependency> | ||
| 51 | + <groupId>org.springframework.boot</groupId> | ||
| 52 | + <artifactId>spring-boot-starter-validation</artifactId> | ||
| 53 | + </dependency> | ||
| 54 | + <dependency> | ||
| 55 | + <groupId>org.springframework.boot</groupId> | ||
| 56 | + <artifactId>spring-boot-starter-aop</artifactId> | ||
| 57 | + </dependency> | ||
| 58 | + <dependency> | ||
| 59 | + <groupId>org.springframework.boot</groupId> | ||
| 60 | + <artifactId>spring-boot-starter-thymeleaf</artifactId> | ||
| 61 | + </dependency> | ||
| 62 | + <dependency> | ||
| 63 | + <groupId>org.springframework.boot</groupId> | ||
| 64 | + <artifactId>spring-boot-starter-data-redis</artifactId> | ||
| 65 | + </dependency> | ||
| 66 | + <dependency> | ||
| 67 | + <groupId>org.apache.commons</groupId> | ||
| 68 | + <artifactId>commons-pool2</artifactId> | ||
| 69 | + </dependency> | ||
| 70 | + <dependency> | ||
| 71 | + <groupId>org.springframework.boot</groupId> | ||
| 72 | + <artifactId>spring-boot-starter-webflux</artifactId> | ||
| 73 | + </dependency> | ||
| 74 | + <dependency> | ||
| 75 | + <groupId>org.springframework.boot</groupId> | ||
| 76 | + <artifactId>spring-boot-starter-test</artifactId> | ||
| 77 | + <scope>test</scope> | ||
| 78 | + </dependency> | ||
| 79 | + <dependency> | ||
| 80 | + <groupId>org.springframework.boot</groupId> | ||
| 81 | + <artifactId>spring-boot-devtools</artifactId> | ||
| 82 | + <optional>true</optional> | ||
| 83 | + </dependency> | ||
| 84 | + | ||
| 85 | + <!-- 数据库 & MyBatis --> | ||
| 86 | + <dependency> | ||
| 87 | + <groupId>org.mybatis.spring.boot</groupId> | ||
| 88 | + <artifactId>mybatis-spring-boot-starter</artifactId> | ||
| 89 | + <version>${mybatis-spring.version}</version> | ||
| 90 | + </dependency> | ||
| 91 | + <dependency> | ||
| 92 | + <groupId>com.mysql</groupId> | ||
| 93 | + <artifactId>mysql-connector-j</artifactId> | ||
| 94 | + <scope>runtime</scope> | ||
| 95 | + </dependency> | ||
| 96 | + <dependency> | ||
| 97 | + <groupId>com.zaxxer</groupId> | ||
| 98 | + <artifactId>HikariCP</artifactId> | ||
| 99 | + </dependency> | ||
| 100 | + <dependency> | ||
| 101 | + <groupId>com.github.pagehelper</groupId> | ||
| 102 | + <artifactId>pagehelper-spring-boot-starter</artifactId> | ||
| 103 | + <version>${pagehelper.version}</version> | ||
| 104 | + </dependency> | ||
| 105 | + <!-- 方案A:使用 pinyin4j(推荐) --> | ||
| 106 | + <dependency> | ||
| 107 | + <groupId>com.belerweb</groupId> | ||
| 108 | + <artifactId>pinyin4j</artifactId> | ||
| 109 | + <version>2.5.1</version> | ||
| 110 | + </dependency> | ||
| 111 | + | ||
| 112 | + <!-- 工具类 --> | ||
| 113 | + <dependency> | ||
| 114 | + <groupId>org.projectlombok</groupId> | ||
| 115 | + <artifactId>lombok</artifactId> | ||
| 116 | + <optional>true</optional> | ||
| 117 | + </dependency> | ||
| 118 | + <dependency> | ||
| 119 | + <groupId>cn.hutool</groupId> | ||
| 120 | + <artifactId>hutool-all</artifactId> | ||
| 121 | + <version>${hutool.version}</version> | ||
| 122 | + </dependency> | ||
| 123 | + <dependency> | ||
| 124 | + <groupId>com.google.code.gson</groupId> | ||
| 125 | + <artifactId>gson</artifactId> | ||
| 126 | + </dependency> | ||
| 127 | + <dependency> | ||
| 128 | + <groupId>commons-io</groupId> | ||
| 129 | + <artifactId>commons-io</artifactId> | ||
| 130 | + <version>2.15.1</version> | ||
| 131 | + </dependency> | ||
| 132 | + <dependency> | ||
| 133 | + <groupId>com.squareup.okhttp3</groupId> | ||
| 134 | + <artifactId>okhttp</artifactId> | ||
| 135 | + <version>${okhttp.version}</version> | ||
| 136 | + </dependency> | ||
| 137 | + | ||
| 138 | + <!-- JSON --> | ||
| 139 | + <dependency> | ||
| 140 | + <groupId>com.alibaba.fastjson2</groupId> | ||
| 141 | + <artifactId>fastjson2</artifactId> | ||
| 142 | + <version>${fastjson.version}</version> | ||
| 143 | + </dependency> | ||
| 144 | + <dependency> | ||
| 145 | + <groupId>com.fasterxml.jackson.core</groupId> | ||
| 146 | + <artifactId>jackson-databind</artifactId> | ||
| 147 | + <version>${jackson.version}</version> | ||
| 148 | + </dependency> | ||
| 149 | + <dependency> | ||
| 150 | + <groupId>com.fasterxml.jackson.core</groupId> | ||
| 151 | + <artifactId>jackson-core</artifactId> | ||
| 152 | + <version>${jackson.version}</version> | ||
| 153 | + </dependency> | ||
| 154 | + <dependency> | ||
| 155 | + <groupId>com.fasterxml.jackson.core</groupId> | ||
| 156 | + <artifactId>jackson-annotations</artifactId> | ||
| 157 | + <version>${jackson.version}</version> | ||
| 158 | + </dependency> | ||
| 159 | + <dependency> | ||
| 160 | + <groupId>com.fasterxml.jackson.datatype</groupId> | ||
| 161 | + <artifactId>jackson-datatype-jsr310</artifactId> | ||
| 162 | + <version>${jackson.version}</version> | ||
| 163 | + </dependency> | ||
| 164 | + | ||
| 165 | + <!-- 文档处理 --> | ||
| 166 | + <dependency> | ||
| 167 | + <groupId>org.apache.tika</groupId> | ||
| 168 | + <artifactId>tika-core</artifactId> | ||
| 169 | + <version>2.9.1</version> | ||
| 170 | + </dependency> | ||
| 171 | + <dependency> | ||
| 172 | + <groupId>org.apache.pdfbox</groupId> | ||
| 173 | + <artifactId>pdfbox</artifactId> | ||
| 174 | + <version>3.0.2</version> | ||
| 175 | + </dependency> | ||
| 176 | + <dependency> | ||
| 177 | + <groupId>org.apache.poi</groupId> | ||
| 178 | + <artifactId>poi-ooxml</artifactId> | ||
| 179 | + <version>5.2.5</version> | ||
| 180 | + </dependency> | ||
| 181 | + | ||
| 182 | + <!-- OCR --> | ||
| 183 | + <dependency> | ||
| 184 | + <groupId>net.sourceforge.tess4j</groupId> | ||
| 185 | + <artifactId>tess4j</artifactId> | ||
| 186 | + <version>${tess4j.version}</version> | ||
| 187 | + </dependency> | ||
| 188 | + <dependency> | ||
| 189 | + <groupId>io.github.mymonstercat</groupId> | ||
| 190 | + <artifactId>rapidocr</artifactId> | ||
| 191 | + <version>0.0.7</version> | ||
| 192 | + </dependency> | ||
| 193 | + <dependency> | ||
| 194 | + <groupId>io.github.mymonstercat</groupId> | ||
| 195 | + <artifactId>rapidocr-onnx-platform</artifactId> | ||
| 196 | + <version>0.0.7</version> | ||
| 197 | + </dependency> | ||
| 198 | + | ||
| 199 | + <!-- 向量数据库 --> | ||
| 200 | + <dependency> | ||
| 201 | + <groupId>io.milvus</groupId> | ||
| 202 | + <artifactId>milvus-sdk-java</artifactId> | ||
| 203 | + <version>${milvus.version}</version> | ||
| 204 | + </dependency> | ||
| 205 | + | ||
| 206 | + <!-- LangChain4j 核心 + Ollama --> | ||
| 207 | + <dependency> | ||
| 208 | + <groupId>dev.langchain4j</groupId> | ||
| 209 | + <artifactId>langchain4j</artifactId> | ||
| 210 | + <version>${langchain4j.version}</version> | ||
| 211 | + </dependency> | ||
| 212 | + <dependency> | ||
| 213 | + <groupId>dev.langchain4j</groupId> | ||
| 214 | + <artifactId>langchain4j-ollama</artifactId> | ||
| 215 | + <version>${langchain4j.version}</version> | ||
| 216 | + </dependency> | ||
| 217 | + <dependency> | ||
| 218 | + <groupId>dev.langchain4j</groupId> | ||
| 219 | + <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> | ||
| 220 | + <version>${langchain4j-emb.version}</version> | ||
| 221 | + </dependency> | ||
| 222 | + | ||
| 223 | + <!-- 分词 --> | ||
| 224 | + <dependency> | ||
| 225 | + <groupId>com.huaban</groupId> | ||
| 226 | + <artifactId>jieba-analysis</artifactId> | ||
| 227 | + <version>1.0.2</version> | ||
| 228 | + </dependency> | ||
| 229 | + | ||
| 230 | + <!-- 接口文档 --> | ||
| 231 | + <dependency> | ||
| 232 | + <groupId>org.springdoc</groupId> | ||
| 233 | + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> | ||
| 234 | + <version>${springdoc.version}</version> | ||
| 235 | + </dependency> | ||
| 236 | + | ||
| 237 | + <!-- MapStruct --> | ||
| 238 | + <dependency> | ||
| 239 | + <groupId>org.mapstruct</groupId> | ||
| 240 | + <artifactId>mapstruct</artifactId> | ||
| 241 | + <version>${mapstruct.version}</version> | ||
| 242 | + </dependency> | ||
| 243 | + | ||
| 244 | + <!-- 其他工具 --> | ||
| 245 | + <dependency> | ||
| 246 | + <groupId>com.github.jsqlparser</groupId> | ||
| 247 | + <artifactId>jsqlparser</artifactId> | ||
| 248 | + <version>4.9</version> | ||
| 249 | + </dependency> | ||
| 250 | + <dependency> | ||
| 251 | + <groupId>org.jsoup</groupId> | ||
| 252 | + <artifactId>jsoup</artifactId> | ||
| 253 | + <version>1.17.2</version> | ||
| 254 | + </dependency> | ||
| 255 | + | ||
| 256 | + <!-- 删掉这两个错误的 --> | ||
| 257 | + <dependency> | ||
| 258 | + <groupId>org.codehaus.groovy</groupId> | ||
| 259 | + <artifactId>groovy</artifactId> | ||
| 260 | + <version>3.0.20</version> | ||
| 261 | + </dependency> | ||
| 262 | + <dependency> | ||
| 263 | + <groupId>org.codehaus.groovy</groupId> | ||
| 264 | + <artifactId>groovy-all</artifactId> | ||
| 265 | + <version>3.0.20</version> | ||
| 266 | + <type>pom</type> | ||
| 267 | + </dependency> | ||
| 268 | + | ||
| 269 | + <!-- 正确的 Fastjson2 --> | ||
| 270 | + <dependency> | ||
| 271 | + <groupId>com.alibaba.fastjson2</groupId> | ||
| 272 | + <artifactId>fastjson2</artifactId> | ||
| 273 | + <version>2.0.50</version> | ||
| 274 | + </dependency> | ||
| 275 | + | ||
| 276 | + <!-- Spring Data JPA 核心依赖(自动包含 jakarta.persistence 包) --> | ||
| 277 | + <dependency> | ||
| 278 | + <groupId>org.springframework.boot</groupId> | ||
| 279 | + <artifactId>spring-boot-starter-data-jpa</artifactId> | ||
| 280 | + </dependency> | ||
| 281 | + | ||
| 282 | + <!-- 数据库驱动(以 MySQL 为例,根据你的数据库替换) --> | ||
| 283 | + <dependency> | ||
| 284 | + <groupId>com.mysql</groupId> | ||
| 285 | + <artifactId>mysql-connector-j</artifactId> | ||
| 286 | + <scope>runtime</scope> | ||
| 287 | + </dependency> | ||
| 288 | + | ||
| 289 | + <dependency> | ||
| 290 | + <groupId>com.github.jnr</groupId> | ||
| 291 | + <artifactId>jnr-ffi</artifactId> | ||
| 292 | + <version>2.2.15</version> <!-- 稳定版 --> | ||
| 293 | + </dependency> | ||
| 294 | + <dependency> | ||
| 295 | + <groupId>jakarta.annotation</groupId> | ||
| 296 | + <artifactId>jakarta.annotation-api</artifactId> | ||
| 297 | + <version>2.1.1</version> | ||
| 298 | + </dependency> | ||
| 299 | + <dependency> | ||
| 300 | + <groupId>org.springframework.retry</groupId> | ||
| 301 | + <artifactId>spring-retry</artifactId> | ||
| 302 | + </dependency> | ||
| 303 | + | ||
| 304 | + <!-- 必须加:Spring Retry 需要 AOP 依赖 --> | ||
| 305 | + <dependency> | ||
| 306 | + <groupId>org.springframework.boot</groupId> | ||
| 307 | + <artifactId>spring-boot-starter-aop</artifactId> | ||
| 308 | + </dependency> | ||
| 309 | + </dependencies> | ||
| 310 | + | ||
| 311 | + <build> | ||
| 312 | + <finalName>xlyAi</finalName> | ||
| 313 | + <resources> | ||
| 314 | + <resource> | ||
| 315 | + <directory>src/main/resources</directory> | ||
| 316 | + <filtering>true</filtering> | ||
| 317 | + <includes> | ||
| 318 | + <include>**/*</include> | ||
| 319 | + </includes> | ||
| 320 | + </resource> | ||
| 321 | + </resources> | ||
| 322 | + | ||
| 323 | + <plugins> | ||
| 324 | + <plugin> | ||
| 325 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 326 | + <artifactId>maven-compiler-plugin</artifactId> | ||
| 327 | + <version>3.11.0</version> | ||
| 328 | + <configuration> | ||
| 329 | + <source>17</source> | ||
| 330 | + <target>17</target> | ||
| 331 | + <encoding>UTF-8</encoding> | ||
| 332 | + <annotationProcessorPaths> | ||
| 333 | + <path> | ||
| 334 | + <groupId>org.projectlombok</groupId> | ||
| 335 | + <artifactId>lombok</artifactId> | ||
| 336 | + <version>${lombok.version}</version> | ||
| 337 | + </path> | ||
| 338 | + <path> | ||
| 339 | + <groupId>org.mapstruct</groupId> | ||
| 340 | + <artifactId>mapstruct-processor</artifactId> | ||
| 341 | + <version>${mapstruct.version}</version> | ||
| 342 | + </path> | ||
| 343 | + </annotationProcessorPaths> | ||
| 344 | + </configuration> | ||
| 345 | + </plugin> | ||
| 346 | + <plugin> | ||
| 347 | + <groupId>org.springframework.boot</groupId> | ||
| 348 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
| 349 | + </plugin> | ||
| 350 | + </plugins> | ||
| 351 | + </build> | ||
| 352 | +</project> | ||
| 0 | \ No newline at end of file | 353 | \ No newline at end of file |
src/main/java/com/xly/XlyAiApplication.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/XlyAiApplication.java | ||
| 1 | +package com.xly; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.ObjectUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import org.mybatis.spring.annotation.MapperScan; | ||
| 6 | +import org.slf4j.Logger; | ||
| 7 | +import org.slf4j.LoggerFactory; | ||
| 8 | +import org.springframework.boot.SpringApplication; | ||
| 9 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| 10 | +import org.springframework.boot.builder.SpringApplicationBuilder; | ||
| 11 | +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; | ||
| 12 | +import org.springframework.cache.annotation.EnableCaching; | ||
| 13 | +import org.springframework.scheduling.annotation.EnableAsync; | ||
| 14 | +import org.springframework.scheduling.annotation.EnableScheduling; | ||
| 15 | +import org.springframework.web.bind.annotation.PostMapping; | ||
| 16 | +import org.springframework.web.bind.annotation.RequestBody; | ||
| 17 | + | ||
| 18 | +import java.util.HashMap; | ||
| 19 | +import java.util.Map; | ||
| 20 | + | ||
| 21 | +@EnableAsync | ||
| 22 | +@EnableScheduling | ||
| 23 | +@EnableCaching | ||
| 24 | +@SpringBootApplication | ||
| 25 | +@MapperScan("com.xly.mapper") | ||
| 26 | +public class XlyAiApplication extends SpringBootServletInitializer { // 关键:继承 SpringBootServletInitializer | ||
| 27 | + | ||
| 28 | + private static final Logger logger = LoggerFactory.getLogger(XlyAiApplication.class); | ||
| 29 | + | ||
| 30 | + @Override // 关键:重写 configure 方法 | ||
| 31 | + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { | ||
| 32 | + return application.sources(XlyAiApplication.class); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 提取报修结构化信息 | ||
| 37 | + */ | ||
| 38 | + @PostMapping("/") | ||
| 39 | + public String home(@RequestBody Map<String,Object> sendMap) { | ||
| 40 | + return "欢迎来到小羚羊AI系统"; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + public static void main(String[] args) { | ||
| 44 | + SpringApplication.run(XlyAiApplication.class, args); | ||
| 45 | + logger.info(" \n" + | ||
| 46 | + " /$$ \n" + | ||
| 47 | + " | $$ \n" + | ||
| 48 | + " /$$ /$$ /$$ /$$ /$$ /$$$$$$$| $$ /$$$$$$$\n" + | ||
| 49 | + "| $$ | $$ | $$| $$ | $$ /$$_____/| $$ /$$_____/\n" + | ||
| 50 | + "| $$ | $$ | $$| $$ | $$| $$ | $$| $$$$$$ \n" + | ||
| 51 | + "| $$ | $$ | $$| $$ | $$| $$ | $$ \\____ $$\n" + | ||
| 52 | + "| $$$$$/$$$$/| $$$$$$$| $$$$$$$| $$ /$$$$$$$/\n" + | ||
| 53 | + " \\_____/\\___/ \\____ $$ \\_______/|__/|_______/ \n" + | ||
| 54 | + " /$$ | $$ \n" + | ||
| 55 | + " | $$$$$$/ \n" + | ||
| 56 | + " \\______/ "); | ||
| 57 | + logger.info("I wish you a pleasant use of the system and never have any bugs"); | ||
| 58 | + } | ||
| 59 | +} | ||
| 0 | \ No newline at end of file | 60 | \ No newline at end of file |
src/main/java/com/xly/agent/AgentSystemPrompt.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/AgentSystemPrompt.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | +public class AgentSystemPrompt { | ||
| 4 | + | ||
| 5 | +// public static final String sSystemPrompt = "你是一个专业的智能助手,请根据以下规则精准匹配并调用工具:\n" + | ||
| 6 | +// "【核心决策逻辑】\n" + | ||
| 7 | +// "1. 单一工具绝对调用:当系统仅提供一个可用工具时,必须无条件调用该工具。即使参数不全,也必须发起调用,严禁返回向用户索要信息或者不调用工具;\n" + | ||
| 8 | +// "2. 多工具意图匹配:当系统提供多个工具时,必须先精准识别用户的核心业务意图,然后匹配唯一对应的工具进行调用;\n" + | ||
| 9 | +// "\n" + | ||
| 10 | +// "【参数提取与调用规范】\n" + | ||
| 11 | +// "3. 触发条件:匹配工具时,仅关注工具描述中“当用户”和“时,必须调用本工具”之间的语义关联;\n" + | ||
| 12 | +// "4. 参数补全与缺省处理:\n" + | ||
| 13 | +// " - 如果当前是全新的独立请求,且必填参数缺失,请直接省略该字段发起调用(不要反问用户)。\n" + | ||
| 14 | +// "5. 格式要求:提取参数时严格按标注类型赋值,数字不带引号,字符串需去除首尾空格;\n" + | ||
| 15 | +// "6. 绝对禁止:在任何情况下,禁止向用户反问以确认参数或意图,你的输出只能是最终回复或合法的工具调用请求。"; | ||
| 16 | + | ||
| 17 | + public static final String sSystemPrompt = "你是一个无情的参数提取器,不是聊天助手。\n" + | ||
| 18 | + "【绝对指令】\n" + | ||
| 19 | + "1. 你的唯一任务是提取用户输入中的参数,并立即调用工具。\n" + | ||
| 20 | + "2. 严禁与用户进行任何对话!严禁输出“您好”、“请提供”、“需要补充”等任何纯文本回复!\n" + | ||
| 21 | + "3. 无论用户输入多么简短(例如只输入了公司名),或者参数缺失多么严重,你都必须直接调用工具,将缺失的字段留空或填0。\n" + | ||
| 22 | + "4. 不要试图去引导用户,引导用户是你的后端程序要做的事,你只需要负责把提取到的参数传出去。"; | ||
| 23 | +} | ||
| 0 | \ No newline at end of file | 24 | \ No newline at end of file |
src/main/java/com/xly/agent/ChatiAgent.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/ChatiAgent.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.service.MemoryId; | ||
| 4 | +import dev.langchain4j.service.SystemMessage; | ||
| 5 | +import dev.langchain4j.service.UserMessage; | ||
| 6 | +import dev.langchain4j.service.V; | ||
| 7 | +import reactor.core.publisher.Flux; | ||
| 8 | + | ||
| 9 | +public interface ChatiAgent { | ||
| 10 | + @SystemMessage(""" | ||
| 11 | + 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | ||
| 12 | + 要求:1. 不生硬、不说教,避免书面化表达; | ||
| 13 | + 2. 主动接梗,适当延伸话题,不一问一答; | ||
| 14 | + 3. 偶尔带点小幽默,保持轻松无压力的氛围; | ||
| 15 | + 4. 回答简洁,符合日常聊天的语气,不啰嗦。 | ||
| 16 | + 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 | ||
| 17 | + """) | ||
| 18 | + @UserMessage("用户说:{{userInput}}") | ||
| 19 | + String chat(@MemoryId String userId, @V("userInput") String userInput); | ||
| 20 | + | ||
| 21 | + @SystemMessage(""" | ||
| 22 | + 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | ||
| 23 | + 要求:1. 不生硬、不说教,避免书面化表达; | ||
| 24 | + 2. 主动接梗,适当延伸话题,不一问一答; | ||
| 25 | + 3. 偶尔带点小幽默,保持轻松无压力的氛围; | ||
| 26 | + 4. 回答简洁,符合日常聊天的语气,不啰嗦。 | ||
| 27 | + 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 | ||
| 28 | + """) | ||
| 29 | + @UserMessage("用户说:{{userInput}}") | ||
| 30 | + Flux<String> chatStream(@MemoryId String userId, @V("userInput") String userInput); | ||
| 31 | +} |
src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.service.MemoryId; | ||
| 4 | +import dev.langchain4j.service.SystemMessage; | ||
| 5 | +import dev.langchain4j.service.UserMessage; | ||
| 6 | +import dev.langchain4j.service.V; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 适配动态表结构的NL2SQL AI服务 | ||
| 10 | + * 核心:表结构作为动态参数传入,@SystemMessage仅保留通用规则 | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +public interface DynamicTableNl2SqlAiAgent { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 动态表结构:自然语言转MySQL SELECT语句 | ||
| 18 | + * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 | ||
| 19 | + */ | ||
| 20 | + @SystemMessage(""" | ||
| 21 | + 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: | ||
| 22 | + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | ||
| 23 | + 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; | ||
| 24 | + 3. 编写规范: | ||
| 25 | + 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | ||
| 26 | + 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | ||
| 27 | + 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; | ||
| 28 | + 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; | ||
| 29 | + 3.5 SQL所有的查询条件,如果是布尔类型的字段 判断值只能是1或者0,1表示:true,0表示:false,例如:viw_ai_quotation.bCheck = 1; | ||
| 30 | + 3.6 SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; | ||
| 31 | + 3.7 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 | ||
| 32 | + 3.8 在AVG聚合函数中不允许使用LAG、LEAD 等窗口函数 例如:AVG(LAG(...))这种嵌套是不允许的 | ||
| 33 | + 3.9 GROUP BY 后面不允许使用窗口函数 | ||
| 34 | + 4. 安全约束: | ||
| 35 | + - 禁止:DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等) | ||
| 36 | + - 禁止:存储过程、自定义函数、临时表 | ||
| 37 | + - 允许:子查询(当需要使用窗口函数LAG/ROW_NUMBER等时) | ||
| 38 | + - 允许:CTE公用表表达式(WITH语句) | ||
| 39 | + 5. 精准性: | ||
| 40 | + 5.1 严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; | ||
| 41 | + 5.2 用户需求中没有明确的日期条件,默认为全部数据,禁止增加任何日期过滤条件 | ||
| 42 | + 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。 | ||
| 43 | + 7. 当前时间:{{sDataNow}} | ||
| 44 | + 8. 时间处理规则: | ||
| 45 | + 8.1 当前系统时间:{{sDataNow}}(格式:yyyy年MM月dd日HH时mm分ss秒) | ||
| 46 | + 8.2 用户需求中的相对时间概念,必须基于{{sDataNow}}进行转换: | ||
| 47 | + - "本年" → 当前年份:{{sDataNow}}的年份 | ||
| 48 | + - "本月" → 当前月份:{{sDataNow}}的年份和月份 | ||
| 49 | + - "本季度" → 当前季度:基于{{sDataNow}}计算 | ||
| 50 | + - "本日/今天" → {{sDataNow}}的具体日期 | ||
| 51 | + - "昨天" → {{sDataNow}}减1天 | ||
| 52 | + - "本周" → 基于{{sDataNow}}计算周一到周日 | ||
| 53 | + - "近7天" → {{sDataNow}}减7天到{{sDataNow}} | ||
| 54 | + 8.3 示例转换: | ||
| 55 | + 当前时间:2024-03-15 | ||
| 56 | + 用户说"查询本年数据" → 查询条件应为:YEAR(日期字段) = 2024 | ||
| 57 | + 用户说"查询本月数据" → 查询条件应为:YEAR(日期字段) = 2024 AND MONTH(日期字段) = 3 | ||
| 58 | + 8.4 如果用户需求中没有明确的时间条件,禁止增加任何时间过滤条件 | ||
| 59 | + """) | ||
| 60 | + @UserMessage(""" | ||
| 61 | + 【业务场景表结构信息】 | ||
| 62 | + 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN) | ||
| 63 | + 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键)) | ||
| 64 | + 当前时间:{{sDataNow}} | ||
| 65 | + 【用户需求】 | ||
| 66 | + {{userInput}} | ||
| 67 | + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句; | ||
| 68 | + 【时间处理要求】 | ||
| 69 | + 1. 如果用户需求包含"本年/本月/本季度/本日/今天/昨天/本周/近X天"等相对时间概念: | ||
| 70 | + - 必须使用当前系统时间 {{sDataNow}} 进行转换 | ||
| 71 | + - 转换为具体的年份、月份或日期范围 | ||
| 72 | + 2. 转换示例: | ||
| 73 | + - "本年" → 年份 = {{sDataNow}}的年份部分 | ||
| 74 | + - "本月" → 年份 = {{sDataNow}}的年份, 月份 = {{sDataNow}}的月份 | ||
| 75 | + - "今天" → 日期 = {{sDataNow}} | ||
| 76 | + 3. 如果没有明确的时间需求,不要添加任何时间过滤条件 | ||
| 77 | + """) | ||
| 78 | + String generateMysqlSql(@MemoryId String userId, | ||
| 79 | + @V("tableNames") String tableNames, | ||
| 80 | + @V("tableStruct") String tableStruct, | ||
| 81 | + @V("sDataNow") String sDataNow, | ||
| 82 | + @V("userInput") String userInput); | ||
| 83 | + | ||
| 84 | + | ||
| 85 | + /** | ||
| 86 | + * 动态表结构:自然语言转MySQL SELECT语句 | ||
| 87 | + * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 | ||
| 88 | + */ | ||
| 89 | + @SystemMessage(""" | ||
| 90 | + 【系统角色】 | ||
| 91 | + 你是资深MySQL 8.0.36数据分析师,严格杜绝以下错误,生成100%可执行的SELECT语句; | ||
| 92 | + 【严格禁止的错误类型(强制遵守)】 | ||
| 93 | + 1. 语法错误: | ||
| 94 | + - 禁止使用COUNT()无参数写法,必须写COUNT(*)、COUNT(1)或COUNT(具体非空字段); | ||
| 95 | + - 禁止在AVG/SUM等聚合函数内嵌套LAG/ROW_NUMBER等窗口函数(如 AVG(dMaterialsPrice - LAG(dMaterialsPrice, 1, dMaterialsPrice))/SUM(viw_ai_purchaseorder.dMaterialsPrice - LAG(viw_ai_purchaseorder.dMaterialsPrice, 1))); | ||
| 96 | + - 禁止ORDER BY中直接使用未别名的窗口函数表达式(如ORDER BY dMaterialsPrice - LAG(...)); | ||
| 97 | + - 禁止GROUP BY字段与SELECT非聚合字段不一致; | ||
| 98 | + 2. 规则违规: | ||
| 99 | + - 禁止字段不写表名前缀(必须是「表名.字段名」格式); | ||
| 100 | + - 禁止日期字段非空判断用ifnull(日期字段,'')<>''(仅字符字段用此写法,日期字段用IS NOT NULL); | ||
| 101 | + - 禁止生成与{{errorSql}}/{{historySqlList}}重复的语句,禁止仅修改排序字段/别名的“伪差异化”,可以使用子查询修复或者修改查询字段,不要使用窗口函数(如 LAG); | ||
| 102 | + - 禁止LAG窗口函数缺失ORDER BY子句(必须按时间字段排序); | ||
| 103 | + - 禁止HAVING条件使用COUNT()无参数写法,禁止过滤条件与业务需求无关; | ||
| 104 | + - 禁止在 AVG/SUM 等聚合函数中嵌套窗口函数; | ||
| 105 | + 3. 编写规范: | ||
| 106 | + - 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | ||
| 107 | + - SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | ||
| 108 | + - SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; | ||
| 109 | + - SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; | ||
| 110 | + - SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; | ||
| 111 | + - SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 | ||
| 112 | + - 在AVG聚合函数中不允许使用LAG、LEAD 等窗口函数 | ||
| 113 | + - AVG(LAG(...))这种嵌套是不允许的 | ||
| 114 | + - GROUP BY 后面不允许使用窗口函数 | ||
| 115 | + 【生成前自检要求】 | ||
| 116 | + 1. 先检查是否违反上述禁止规则,再生成SQL; | ||
| 117 | + 2. 每条SQL生成后,模拟MySQL执行逻辑自检: | ||
| 118 | + - 语法是否合法(无COUNT()、聚合嵌套窗口函数等); | ||
| 119 | + - 字段是否存在于表结构中; | ||
| 120 | + - 差异化是否满足「结构/函数/格式」2个维度; | ||
| 121 | + 3. 若自检发现错误,立即重新生成,直至所有SQL符合要求。 | ||
| 122 | + """) | ||
| 123 | + @UserMessage(""" | ||
| 124 | + 【业务场景表结构信息】 | ||
| 125 | + 涉及表名:{{tableNames}} | ||
| 126 | + 表结构详情:{{tableStruct}} | ||
| 127 | + 当前时间:{{sDataNow}} | ||
| 128 | + 【原始用户需求】 | ||
| 129 | + {{userInput}} | ||
| 130 | + 【错误信息】 | ||
| 131 | + 之前生成的错误SQL:{{errorSql}} | ||
| 132 | + 执行错误信息:{{errorMessage}} | ||
| 133 | + 【生成要求】 | ||
| 134 | + 1. 先修复错误SQL的所有问题,确保语法/逻辑合规; | ||
| 135 | + 2. 生成与{{errorSql}}/{{historySqlList}}不重复的语句,禁止仅修改排序字段/别名的“伪差异化”,可以使用子查询修复或者修改查询字段,不要使用窗口函数(如 LAG)的SELECT语句; | ||
| 136 | + 3. SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | ||
| 137 | + 4. 自连接+子查询方式时,子查询跟自连接关联关系字段需要在子查询中查询列出现 | ||
| 138 | + 5. 生成语法 / 逻辑合规且与历史语句无 “伪差异化” 的 SELECT 语句,且不使用窗口函数。 | ||
| 139 | + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句;并且仅返回SQL语句 | ||
| 140 | + """) | ||
| 141 | + String regenerateSqlWithError(@MemoryId String userId, | ||
| 142 | + @V("tableNames") String tableNames, | ||
| 143 | + @V("tableStruct") String tableStruct, | ||
| 144 | + @V("sDataNow") String sDataNow, | ||
| 145 | + @V("userInput") String userInput, | ||
| 146 | + @V("errorSql") String errorSql, | ||
| 147 | + @V("errorMessage") String errorMessage, | ||
| 148 | + @V("n") String iErroCount, | ||
| 149 | + @V("historySqlList") String historySqlList | ||
| 150 | + ); | ||
| 151 | +// /** | ||
| 152 | +// * 动态表结构:自然语言解释SQL执行结果 | ||
| 153 | +// * 入参:用户问题、执行的SQL、表结构、JSON格式结果 | ||
| 154 | +// */ | ||
| 155 | +// @SystemMessage(""" | ||
| 156 | +// 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景: | ||
| 157 | +// 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; | ||
| 158 | +// 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; | ||
| 159 | +// 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”; | ||
| 160 | +// 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; | ||
| 161 | +// 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 | ||
| 162 | +// """) | ||
| 163 | +// @UserMessage(""" | ||
| 164 | +// 【业务场景表结构信息】 | ||
| 165 | +// 表结构详情:{{tableStruct}} | ||
| 166 | +// 【查询相关信息】 | ||
| 167 | +// 用户原始查询:{{userInput}} | ||
| 168 | +// 执行的MySQL SQL:{{sql}} | ||
| 169 | +// SQL执行结果(JSON格式):{{result}} | ||
| 170 | +// 请根据上述信息+通用规则,对查询结果做业务解释: | ||
| 171 | +// """) | ||
| 172 | +// String explainSqlResult(@MemoryId String userId, | ||
| 173 | +// @V("userInput") String userInput, | ||
| 174 | +// @V("sql") String sql, | ||
| 175 | +// @V("tableStruct") String tableStruct, | ||
| 176 | +// @V("result") String result); | ||
| 177 | +} | ||
| 0 | \ No newline at end of file | 178 | \ No newline at end of file |
src/main/java/com/xly/agent/ErpAiAgent.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/ErpAiAgent.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import dev.langchain4j.service.*; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 优化后:新增场景专属交互规则,大模型仅处理当前场景业务指令 | ||
| 8 | + */ | ||
| 9 | +public interface ErpAiAgent { | ||
| 10 | + | ||
| 11 | + @SystemMessage("{{sSystemPrompt}}") | ||
| 12 | + @UserMessage("用户输入:{{userInput}}") | ||
| 13 | + Result<String> chat( | ||
| 14 | + @MemoryId String userId, | ||
| 15 | + @V("userInput") String userInput, | ||
| 16 | + @V("sSystemPrompt") String sSystemPrompt | ||
| 17 | + ); | ||
| 18 | + /** | ||
| 19 | + * 动态表结构:自然语言解释SQL执行结果 | ||
| 20 | + * 入参:用户问题、执行的SQL、表结构、JSON格式结果 | ||
| 21 | + */ | ||
| 22 | + @SystemMessage(""" | ||
| 23 | + 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景: | ||
| 24 | + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; | ||
| 25 | + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; | ||
| 26 | + 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据” | ||
| 27 | + 3.1. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 1000000,错误写法是 1,000,000,即使数字很大,也请保持连续的数字串,不要打断。 | ||
| 28 | + 3.2 所有日期请转换为 YYYY-MM-DD 格式(例如:2026-03-15),严禁包含时间部分(如小时、分钟、秒)(例如:2026-03-15 00:00:00),也不要包含时区信息。” | ||
| 29 | + 3.3. 金额,单价,数量 严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 2400056,错误写法是 2,400,056 即使数字很大,也请保持连续的数字串,不要打断。 | ||
| 30 | + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; | ||
| 31 | + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 | ||
| 32 | + """) | ||
| 33 | + @UserMessage(""" | ||
| 34 | + 【业务场景表结构信息】 | ||
| 35 | + 表结构详情:{{tableStruct}} | ||
| 36 | + 【查询相关信息】 | ||
| 37 | + 用户原始查询:{{userInput}} | ||
| 38 | + 执行的MySQL SQL:{{sql}} | ||
| 39 | + SQL执行结果(JSON格式):{{result}} | ||
| 40 | + 请根据上述信息+通用规则,对查询结果做业务解释: | ||
| 41 | + """) | ||
| 42 | + String explainSqlResult(@MemoryId String userId, | ||
| 43 | + @V("userInput") String userInput, | ||
| 44 | + @V("sql") String sql, | ||
| 45 | + @V("tableStruct") String tableStruct, | ||
| 46 | + @V("result") String result); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 动态表结构:自然语言解释SQL执行结果 | ||
| 50 | + * 入参:用户问题、执行的SQL、表结构、JSON格式结果 | ||
| 51 | + */ | ||
| 52 | + @SystemMessage(""" | ||
| 53 | + 你是专业的业务数据分析师,请分析以下查询结果: | ||
| 54 | + 【用户问题】 | ||
| 55 | + {{userInput}} | ||
| 56 | + 【数据字段说明】 | ||
| 57 | + {{sMilvusFiledDescription}} | ||
| 58 | + 【查询结果数据(JSON格式)】 | ||
| 59 | + {{result}} | ||
| 60 | + 【分析要求】 | ||
| 61 | + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; | ||
| 62 | + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; | ||
| 63 | + 3. 输出格式: | ||
| 64 | + 3.1. 如果用户要求"表格形式展示",先输出简短文字说明,然后输出Markdown格式的表格 | ||
| 65 | + 3.2. 如果用户未要求表格,仅返回解释内容,不要列出ID,无多余标题、换行、符号 | ||
| 66 | + 3.3. 结果为空时直接返回"未查询到相关数据" | ||
| 67 | + 3.4. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",") | ||
| 68 | + 3.5. 所有日期请转换为 YYYY-MM-DD 格式,严禁包含时间部分 | ||
| 69 | + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; | ||
| 70 | + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 | ||
| 71 | + """) | ||
| 72 | + @UserMessage(""" | ||
| 73 | + 【用户查询】 | ||
| 74 | + {{userInput}} | ||
| 75 | + 【字段说明】 | ||
| 76 | + {{sMilvusFiledDescription}} | ||
| 77 | + 【查询结果】 | ||
| 78 | + 用户原始查询:{{userInput}} | ||
| 79 | + 执行查询向量库后结果(JSON格式):{{result}} | ||
| 80 | + 请根据上述信息+通用规则,对查询结果做业务解释: | ||
| 81 | + """) | ||
| 82 | + String explainMilvusResult(@MemoryId String userId, | ||
| 83 | + @V("userInput") String userInput, | ||
| 84 | + @V("sMilvusFiledDescription") String sMilvusFiledDescription, | ||
| 85 | + @V("result") String result); | ||
| 86 | + | ||
| 87 | + @SystemMessage(""" | ||
| 88 | + 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false。 | ||
| 89 | + 【最高优先级规则 - 必须首先判断】 | ||
| 90 | + 如果用户需求包含以下任一关键词,**直接返回 false**,不再进行其他判断: | ||
| 91 | + - 明细、列表、清单 | ||
| 92 | + - ...明细、...列表、...清单 | ||
| 93 | + 重要:只要出现以上关键词,说明用户需要的是明细数据查询,而非统计分析。 | ||
| 94 | + 【统计类关键词 - 仅在满足最高优先级规则后才判断】 | ||
| 95 | + 只有当用户需求不包含上述明细类关键词时,才检查是否包含以下关键词: | ||
| 96 | + 统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组 | ||
| 97 | + - 如果包含,返回 true | ||
| 98 | + - 否则返回 false | ||
| 99 | + 【判断示例】 | ||
| 100 | + - \"查询中科精工集团的彩盒类产品的报价单明细\" → false(包含\"明细\") | ||
| 101 | + - \"查询客户张三信息\" → false(无统计关键词,无明细关键词) | ||
| 102 | + - \"销售额排名前10的产品\" → true(包含\"排名\",且无明细关键词) | ||
| 103 | + - \"查看销售订单明细\" → false(包含\"明细\") | ||
| 104 | + """) | ||
| 105 | + @UserMessage(""" | ||
| 106 | + 【用户需求】 | ||
| 107 | + {{userInput}} | ||
| 108 | + """) | ||
| 109 | +// @SystemMessage(""" | ||
| 110 | +// 你是一个智能查询路由专家,请根据【用户需求】,基于**查询效率最优**原则,自动判断使用关系型数据库(MySQL)还是向量库(Milvus),只返回 true 或 false。 | ||
| 111 | +// 满足如下规则中任意一条则返回true 否则返回false: | ||
| 112 | +// - 查询涉及**排名、TOP N、求和、计数、平均值、最大值、总额、最小值、最高、最低、占比** | ||
| 113 | +// - 查询涉及**分组统计(GROUP BY)、排序(ORDER BY)、分页(LIMIT)** | ||
| 114 | +// """) | ||
| 115 | +// @UserMessage(""" | ||
| 116 | +// 【用户需求】 | ||
| 117 | +// {{userInput}} | ||
| 118 | +// """) | ||
| 119 | + Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput); | ||
| 120 | + | ||
| 121 | + /** | ||
| 122 | + * 生成 Milvus 过滤条件(适配 Milvus v2.3.9) | ||
| 123 | + */ | ||
| 124 | + @SystemMessage(""" | ||
| 125 | + MILVUS 查询条件生成规则: | ||
| 126 | + | ||
| 127 | + 【最高优先级 - 输出格式铁律】 | ||
| 128 | + ⚠️ 你的【全部输出】必须是且仅是一个合法的 JSON 对象 | ||
| 129 | + ⚠️ 禁止输出任何解释、说明、思考过程 | ||
| 130 | + ⚠️ 禁止输出任何中文文字 | ||
| 131 | + ⚠️ 只能输出以下 JSON 格式,不能有其他任何内容 | ||
| 132 | + | ||
| 133 | + 【输出 JSON 结构】 | ||
| 134 | + { | ||
| 135 | + "sMethodName": true/false, // 必选,判断用户意图是否匹配当前方法 | ||
| 136 | + "vectorField": "向量字段名", // 可选,需要语义匹配时返回 | ||
| 137 | + "vectorValue": "向量化文本", // 可选,用于向量检索的文本 | ||
| 138 | + "filterExpression": "标量过滤表达式" // 可选,有标量条件时返回 | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + 【方法匹配规则 - 重要】 | ||
| 142 | + sMethodName 的取值逻辑: | ||
| 143 | + - 如果用户输入的意图与 {{sMethodName}} 相关,返回 true | ||
| 144 | + - 否则返回 false | ||
| 145 | + | ||
| 146 | + 判断标准:用户是否在询问或操作与 {{sMethodName}} 相关的业务数据 | ||
| 147 | + - 包括但不限于:查询、搜索、推荐、找、查看、明细、列表、详情等 | ||
| 148 | + - 只要用户想获取或操作这类数据,就应该返回 true | ||
| 149 | + | ||
| 150 | + 示例: | ||
| 151 | + - 方法名称:查询报价单 | ||
| 152 | + - "报价单明细" → true(想查看报价单数据) | ||
| 153 | + - "报价单列表" → true(想查看报价单数据) | ||
| 154 | + - "报价单" → true(想查看报价单数据) | ||
| 155 | + - "查一下报价单" → true(想查询报价单) | ||
| 156 | + - "推荐相似报价单" → true(想推荐报价单) | ||
| 157 | + - "你好" → false(与报价单无关) | ||
| 158 | + - "查询客户信息" → false(与报价单无关) | ||
| 159 | + | ||
| 160 | + 【重要:理解你的数据结构】 | ||
| 161 | + | ||
| 162 | + 你有两种类型的字段: | ||
| 163 | + | ||
| 164 | + 1. 标量字段(用于精确过滤): | ||
| 165 | + - {{sMilvusFiled}} 中的字段 | ||
| 166 | + - 根据提供的字段说明使用 | ||
| 167 | + | ||
| 168 | + 2. 向量字段(用于语义搜索): | ||
| 169 | + - 字段名:使用 {{sMilvusFiledXl}} 中提供的向量字段名 | ||
| 170 | + - 存储格式:管道符分隔的键值对 "字段名:值|字段名:值|..." | ||
| 171 | + - 包含的业务数据:{{sMilvusFiledDescriptionXl}} | ||
| 172 | + - 示例:"sCustomerName:上海小羚羊|sProductName:2028宣传海报|dProductQty:1000|sType:报价丢单原因" | ||
| 173 | + | ||
| 174 | + 【向量搜索规则】 | ||
| 175 | + | ||
| 176 | + 使用向量搜索的条件:用户明确表达语义匹配意图 | ||
| 177 | + - 关键词:找相似、推荐、匹配、类似、相关、类似的、相似的、推荐一下 | ||
| 178 | + - 使用向量搜索时: | ||
| 179 | + 1. 从用户问题中提取关键业务实体 | ||
| 180 | + 2. 格式化为:"字段名:值|字段名:值"(管道符分隔) | ||
| 181 | + 3. 示例: | ||
| 182 | + - 用户:"找丢单原因类似价格太高的记录" → "sReason:价格太高|sType:报价丢单原因" | ||
| 183 | + - 用户:"推荐和上海小羚羊类似的客户" → "sCustomerName:上海小羚羊" | ||
| 184 | + - 用户:"找类似2028宣传海报的产品" → "sProductName:2028宣传海报" | ||
| 185 | + | ||
| 186 | + 【标量过滤规则 - 极其重要】 | ||
| 187 | + | ||
| 188 | + 可用标量字段(根据 {{sMilvusFiled}} 动态提供): | ||
| 189 | + - 使用提供的字段名 | ||
| 190 | + - 【字符串字段】:统一使用 like 操作符(前缀匹配) | ||
| 191 | + - 【数字字段】:使用 >、<、>=、<=、== 操作符 | ||
| 192 | + - 【时间字段】:使用时间戳范围 | ||
| 193 | + | ||
| 194 | + 【操作符使用规则】 | ||
| 195 | + | ||
| 196 | + 1. 字符串字段(统一使用 like): | ||
| 197 | + - 语法:字段名 like '关键词%' | ||
| 198 | + - 说明:只支持前缀匹配,不支持 '%keyword%' 或 '%keyword' | ||
| 199 | + - 示例: | ||
| 200 | + - 用户:"上海小羚羊" → sCustomerName like '上海小羚羊%' | ||
| 201 | + - 用户:"客户名称包含上海" → sCustomerName like '上海%' | ||
| 202 | + - 用户:"姓张的销售人员" → sSalesManName like '张%' | ||
| 203 | + - 用户:"产品名称以2028开头" → sProductName like '2028%' | ||
| 204 | + - 用户:"张三" → sSalesManName like '张三%' | ||
| 205 | + | ||
| 206 | + 2. 数字字段: | ||
| 207 | + - 精确匹配:字段名 == 值 | ||
| 208 | + - 范围匹配:字段名 >= 值 && 字段名 <= 值 | ||
| 209 | + - 大于:字段名 > 值 | ||
| 210 | + - 小于:字段名 < 值 | ||
| 211 | + - 示例: | ||
| 212 | + - 用户:"数量1000" → dProductQty == 1000 | ||
| 213 | + - 用户:"数量大于1000" → dProductQty > 1000 | ||
| 214 | + - 用户:"数量在500到1000之间" → dProductQty >= 500 && dProductQty <= 1000 | ||
| 215 | + | ||
| 216 | + 3. 时间字段: | ||
| 217 | + - 使用时间戳范围:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳 | ||
| 218 | + - 时间戳为 Unix 秒 | ||
| 219 | + | ||
| 220 | + 4. 组合条件: | ||
| 221 | + - 使用 && (AND) 连接多个条件 | ||
| 222 | + - 使用 || (OR) 连接或条件 | ||
| 223 | + - 复杂条件用括号分组 | ||
| 224 | + | ||
| 225 | + 【核心约束 - 必须遵守】 | ||
| 226 | + 1. 只有当用户【明确指定】具体条件时,才生成 filterExpression | ||
| 227 | + 2. 禁止为模糊查询添加默认过滤条件(如时间范围) | ||
| 228 | + 3. 禁止自动添加任何默认条件 | ||
| 229 | + 4. 当前时间戳:{{sDataNow}}(Unix秒) | ||
| 230 | + 5. 字符串字段禁止使用 ==,必须使用 like | ||
| 231 | + 6. like 只支持前缀匹配,不支持 '%keyword%' 或 '%keyword' 格式 | ||
| 232 | + | ||
| 233 | + 【查询意图判断】 | ||
| 234 | + | ||
| 235 | + 类型A - 纯标量查询(只返回 filterExpression): | ||
| 236 | + - 用户明确指定了标量字段的具体条件 | ||
| 237 | + - 不包含语义匹配词 | ||
| 238 | + - 示例1:"上海小羚羊" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"} | ||
| 239 | + - 示例2:"客户名称包含上海" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"} | ||
| 240 | + - 示例3:"张三" → {"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"} | ||
| 241 | + - 示例4:"数量大于1000" → {"sMethodName": true, "filterExpression": "dProductQty > 1000"} | ||
| 242 | + - 示例5:"今天创建的报价单" → {"sMethodName": true, "filterExpression": "tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳"} | ||
| 243 | + | ||
| 244 | + 类型B - 纯向量搜索(只返回 vectorField 和 vectorValue): | ||
| 245 | + - 用户只有语义意图,无具体标量条件 | ||
| 246 | + - 示例:"找一些类似的报价单" → {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sType:报价丢单原因"} | ||
| 247 | + | ||
| 248 | + 类型C - 混合查询(同时返回 vectorField、vectorValue 和 filterExpression): | ||
| 249 | + - 用户既有具体条件,又需要语义匹配 | ||
| 250 | + - 示例:"找丢单原因类似价格太高的上海小羚羊报价单" → | ||
| 251 | + {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"} | ||
| 252 | + | ||
| 253 | + 类型D - 意图不匹配(只返回 sMethodName: false): | ||
| 254 | + - 用户输入与 {{sMethodName}} 完全无关 | ||
| 255 | + - 示例:"你好" → {"sMethodName": false} | ||
| 256 | + - 示例:"查询客户信息"(如果方法名是"查询报价单") → {"sMethodName": false} | ||
| 257 | + | ||
| 258 | + 类型E - 意图匹配但无具体条件(只返回 sMethodName: true): | ||
| 259 | + - 用户想查询该类数据,但没有指定任何条件 | ||
| 260 | + - 示例:"报价单明细" → {"sMethodName": true} | ||
| 261 | + - 示例:"报价单列表" → {"sMethodName": true} | ||
| 262 | + - 示例:"报价单" → {"sMethodName": true} | ||
| 263 | + | ||
| 264 | + 【时间处理规则】 | ||
| 265 | + | ||
| 266 | + 当用户明确提到时间时,基于当前时间 {{sDataNow}} 计算: | ||
| 267 | + - "今天":当天 00:00:00 到 23:59:59 | ||
| 268 | + - "昨天":前一天 00:00:00 到 23:59:59 | ||
| 269 | + - "本周":本周一 00:00:00 到本周日 23:59:59 | ||
| 270 | + - "本月":本月1日 00:00:00 到本月最后一天 23:59:59 | ||
| 271 | + - "上个月":上个月1日 00:00:00 到上个月最后一天 23:59:59 | ||
| 272 | + - "近X天":X天前 00:00:00 到今天 23:59:59 | ||
| 273 | + | ||
| 274 | + 时间范围表达式格式:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳 | ||
| 275 | + | ||
| 276 | + 【重要约束汇总】 | ||
| 277 | + 1. 只输出 JSON,不要有任何其他内容 | ||
| 278 | + 2. 只使用提供的标量字段 | ||
| 279 | + 3. 【关键】字符串字段必须使用 like,禁止使用 == | ||
| 280 | + 4. 时间范围必须使用 Unix 时间戳(秒) | ||
| 281 | + 5. 向量字段名使用 {{sMilvusFiledXl}} 中提供的字段名 | ||
| 282 | + 6. vectorValue 必须保持管道符分隔格式:"字段名:值|字段名:值" | ||
| 283 | + 7. 禁止为模糊查询添加默认条件 | ||
| 284 | + 8. sMethodName 只要用户意图与当前方法相关就返回 true | ||
| 285 | + 9. like 只支持前缀匹配,格式:字段名 like '值%' | ||
| 286 | + | ||
| 287 | + 【输出示例】 | ||
| 288 | + 方法名称:查询报价单 | ||
| 289 | + | ||
| 290 | + 示例1:模糊查询 - "报价单明细" | ||
| 291 | + 输出:{"sMethodName": true} | ||
| 292 | + | ||
| 293 | + 示例2:字符串模糊匹配 - "上海小羚羊" | ||
| 294 | + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"} | ||
| 295 | + | ||
| 296 | + 示例3:字符串模糊匹配 - "客户名称包含上海" | ||
| 297 | + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"} | ||
| 298 | + | ||
| 299 | + 示例4:字符串模糊匹配 - "张三" | ||
| 300 | + 输出:{"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"} | ||
| 301 | + | ||
| 302 | + 示例5:数字范围 - "数量大于1000" | ||
| 303 | + 输出:{"sMethodName": true, "filterExpression": "dProductQty > 1000"} | ||
| 304 | + | ||
| 305 | + 示例6:时间范围 - "今天创建的报价单" | ||
| 306 | + 输出:{"sMethodName": true, "filterExpression": "tCreateDate >= 1774754400 && tCreateDate <= 1774840799"} | ||
| 307 | + | ||
| 308 | + 示例7:组合条件 - "上海小羚羊且数量大于1000" | ||
| 309 | + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%' && dProductQty > 1000"} | ||
| 310 | + | ||
| 311 | + 示例8:纯向量 - "找一些类似的报价单" | ||
| 312 | + 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sType:报价丢单原因"} | ||
| 313 | + | ||
| 314 | + 示例9:混合查询 - "找丢单原因类似价格太高的上海小羚羊报价单" | ||
| 315 | + 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"} | ||
| 316 | + | ||
| 317 | + 示例10:意图不匹配 - "你好" | ||
| 318 | + 输出:{"sMethodName": false} | ||
| 319 | + | ||
| 320 | + 【标量字段列表】 | ||
| 321 | + {{sMilvusFiled}} | ||
| 322 | + | ||
| 323 | + 【标量字段说明】 | ||
| 324 | + {{sMilvusFiledDescription}} | ||
| 325 | + | ||
| 326 | + 【向量字段列表】 | ||
| 327 | + {{sMilvusFiledXl}} | ||
| 328 | + | ||
| 329 | + 【向量字段说明】 | ||
| 330 | + {{sMilvusFiledDescriptionXl}} | ||
| 331 | + | ||
| 332 | + 【方法名称】 | ||
| 333 | + {{sMethodName}} | ||
| 334 | + | ||
| 335 | + """) | ||
| 336 | + @UserMessage(""" | ||
| 337 | + 【用户输入】 | ||
| 338 | + {{userInput}} | ||
| 339 | + | ||
| 340 | + 【当前时间】 | ||
| 341 | + {{sDataNow}} | ||
| 342 | + | ||
| 343 | + 【标量字段】 | ||
| 344 | + {{sMilvusFiled}} | ||
| 345 | + | ||
| 346 | + 【标量字段说明】 | ||
| 347 | + {{sMilvusFiledDescription}} | ||
| 348 | + | ||
| 349 | + 【向量字段】 | ||
| 350 | + {{sMilvusFiledXl}} | ||
| 351 | + | ||
| 352 | + 【向量字段说明】 | ||
| 353 | + {{sMilvusFiledDescriptionXl}} | ||
| 354 | + | ||
| 355 | + 【方法名称】 | ||
| 356 | + {{sMethodName}} | ||
| 357 | + | ||
| 358 | + 请根据以上规则,输出 JSON 格式结果。 | ||
| 359 | +""") | ||
| 360 | + String getMilvusFilter(@MemoryId String userId, | ||
| 361 | + @V("userInput") String userInput, | ||
| 362 | + @V("sMilvusFiled") String sMilvusFiled, | ||
| 363 | + @V("sMilvusFiledDescription") String sMilvusFiledDescription, | ||
| 364 | + @V("sMilvusFiledXl") String sMilvusFiledXl, | ||
| 365 | + @V("sMilvusFiledDescriptionXl") String sMilvusFiledDescriptionXl, | ||
| 366 | + @V("sDataNow") long sDataNow, | ||
| 367 | + @V("sMethodName") String sMethodName | ||
| 368 | + ); | ||
| 369 | +} |
src/main/java/com/xly/agent/SceneSelectorAiAgent.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/SceneSelectorAiAgent.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | +import com.xly.entity.SceneIntentParseResp; | ||
| 4 | +import dev.langchain4j.service.SystemMessage; | ||
| 5 | +import dev.langchain4j.service.UserMessage; | ||
| 6 | +import dev.langchain4j.service.V; | ||
| 7 | +import org.springframework.stereotype.Component; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 场景意图解析AI服务:专门让大模型解析用户输入的意图,匹配对应的业务场景 | ||
| 11 | + * 基于LangChain4j AiServices构建,由大模型返回标准化的场景编码 | ||
| 12 | + */ | ||
| 13 | +public interface SceneSelectorAiAgent { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 核心方法:解析用户意图,匹配业务场景 | ||
| 17 | + * @param userInput 用户输入 | ||
| 18 | + * @param authScenesDesc 可访问场景描述 | ||
| 19 | + * @return 标准化的意图解析响应(仅返回sceneCode) | ||
| 20 | + */ | ||
| 21 | + @SystemMessage(""" | ||
| 22 | + 你是专业的ERP系统**意图解析助理**,你的唯一职责是根据用户输入,匹配其意图对应的业务场景,严格遵循以下规则: | ||
| 23 | + 1. 仅从用户提供的「可访问场景列表」中选择匹配的场景,绝对不允许虚构场景; | ||
| 24 | + 2. 匹配规则:用户输入的意图与场景的「支持功能」高度相关,即匹配该场景; | ||
| 25 | + 3. 输出格式:必须严格返回JSON格式,仅包含sceneCode字段,无任何多余文字、解释、换行; | ||
| 26 | + - 匹配到一个场景:sceneCode为场景码(如salemange/purchasemange/ProductionR&Dmanagement),scene为场景名称(如销售管理/采购管理/生产管理); | ||
| 27 | + - 匹配到多个场景,请列出并让客户选择场景; | ||
| 28 | + - 无匹配场景/用户仅问候:sceneCode为NO_MATCH; | ||
| 29 | + 4. 不允许添加任何额外字段,不允许返回JSON以外的内容,确保后端能直接解析; | ||
| 30 | + 5. 忽略用户输入中的无关语气词(如“你好”“帮我”“麻烦”),提取核心业务意图。 | ||
| 31 | + """) | ||
| 32 | + @UserMessage(""" | ||
| 33 | + 用户输入:{{userInput}} | ||
| 34 | + 可访问场景列表:{{authScenesDesc}} | ||
| 35 | + 请严格按指定格式返回匹配的场景编码! | ||
| 36 | + """) | ||
| 37 | + SceneIntentParseResp parseSceneIntent(@V("userInput") String userInput, | ||
| 38 | + @V("authScenesDesc") String authScenesDesc); | ||
| 39 | +} | ||
| 0 | \ No newline at end of file | 40 | \ No newline at end of file |
src/main/java/com/xly/agent/SystemPromptGenerator.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/agent/SystemPromptGenerator.java | ||
| 1 | +package com.xly.agent; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.ObjectUtil; | ||
| 4 | +import com.alibaba.fastjson2.JSON; | ||
| 5 | +import com.alibaba.fastjson2.JSONObject; | ||
| 6 | +import com.alibaba.fastjson2.JSONWriter; | ||
| 7 | +import com.xly.entity.ParamRule; | ||
| 8 | +import com.xly.entity.UserSceneSession; | ||
| 9 | + | ||
| 10 | +import java.util.List; | ||
| 11 | +import java.util.stream.Collectors; | ||
| 12 | + | ||
| 13 | +public class SystemPromptGenerator { | ||
| 14 | + | ||
| 15 | + public static String generate(String toolName, UserSceneSession session) { | ||
| 16 | + StringBuilder sb = new StringBuilder(); | ||
| 17 | + | ||
| 18 | + sb.append("你是一个无情的参数提取器,不是聊天助手。\n\n"); | ||
| 19 | + sb.append("【核心任务】\n"); | ||
| 20 | + sb.append("从用户输入中提取参数,并立即调用工具 ").append(toolName).append("。\n\n"); | ||
| 21 | + sb.append("【绝对指令】\n"); | ||
| 22 | + sb.append("1. 严禁与用户对话,严禁输出任何纯文本回复。\n"); | ||
| 23 | + sb.append("2. 无论参数缺失多么严重,都必须直接调用工具,缺失字段填空字符串或0。\n"); | ||
| 24 | + sb.append("3. 不要引导用户,后端程序会处理引导逻辑。\n\n"); | ||
| 25 | + sb.append("【参数提取规则】\n"); | ||
| 26 | + sb.append("用户输入格式为 \"参数名: 值\" 或 \"参数名 值\",你必须识别参数名并提取对应的值。\n\n"); | ||
| 27 | + sb.append("【可提取的参数列表】\n"); | ||
| 28 | + List<ParamRule> dynamicParamRuleAll = session.getCurrentTool().getParamRuleListAll(); | ||
| 29 | + // 从参数定义中提取 | ||
| 30 | + for (ParamRule paramRule : dynamicParamRuleAll) { | ||
| 31 | + sb.append("- ").append(paramRule.getSParam()).append("\n"); | ||
| 32 | + } | ||
| 33 | + sb.append("\n【提取示例】\n"); | ||
| 34 | + // 动态生成示例(取前3个参数作为示例) | ||
| 35 | + int count = 0; | ||
| 36 | + for (ParamRule paramRule : dynamicParamRuleAll) { | ||
| 37 | + if (count++ >= 3) break; | ||
| 38 | + sb.append("- 用户输入 \"").append(paramRule.getSParam()).append(": {值}\" → 提取 ").append(paramRule.getSParam()).append("\n"); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + if(ObjectUtil.isNotEmpty(session.getArgs())){ | ||
| 42 | + sb.append("【已提取参数】\n"); | ||
| 43 | + sb.append(JSON.toJSONString(session.getArgs(), JSONWriter.Feature.PrettyFormat)); | ||
| 44 | + } | ||
| 45 | + sb.append("\n【工具调用规则】\n"); | ||
| 46 | + sb.append("1. 每次只调用一次工具\n"); | ||
| 47 | + sb.append("2. 调用后立即停止,不要重复调用\n"); | ||
| 48 | + sb.append("3. 不要总结,不要回复用户,只调用工具"); | ||
| 49 | + | ||
| 50 | + return sb.toString(); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 缺失参数统一提示模板:强制AI调用自定义方法,一次性回填所有参数 | ||
| 55 | + */ | ||
| 56 | + public static String buildMissParamPrompt(UserSceneSession session, List<ParamRule> paramRuleDataMiss) { | ||
| 57 | + String methodNo = session.getCurrentTool().getSMethodNo(); | ||
| 58 | + // 1. 拼接缺失参数的描述(增加“参数名”和“英文名”的对应,方便模型映射) | ||
| 59 | + String paramDesc = paramRuleDataMiss.stream() | ||
| 60 | + .map(p -> String.format("- 参数名:%s (对应字段: %s),示例值:%s", | ||
| 61 | + p.getSParam(), p.getSParamValue(), p.getSExampleValue())) | ||
| 62 | + .collect(Collectors.joining("\n")); | ||
| 63 | + // 但根据你的需求,其实 prompt 里并不需要强行插入一个空的 JSON 片段, | ||
| 64 | + // 直接告诉模型去“合并”即可。 | ||
| 65 | + return String.format(""" | ||
| 66 | + 请你根据用户最新的输入,补充缺失的参数,并带着【所有完整参数】立即发起工具调用。 | ||
| 67 | + | ||
| 68 | + 【核心执行规则】 | ||
| 69 | + 1. **参数合并(至关重要)**:你必须保留上一轮对话中已经获取的参数(如 sSlaveId, operateType 等),并将用户本次提供的新参数与旧参数合并。 | ||
| 70 | + 2. **必须调用工具**:必须调用工具 `%s`。绝对禁止直接输出 JSON 文本,也绝对禁止向用户反问或索要信息。 | ||
| 71 | + 3. **完整调用**:请直接生成包含所有参数的 tool_calls 请求。 | ||
| 72 | + 【已获取数据】 | ||
| 73 | + %s | ||
| 74 | + 【当前缺失的参数说明】 | ||
| 75 | + %s | ||
| 76 | + """, methodNo,JSONObject.toJSONString(session.getArgs()), paramDesc); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + public static String buildDynamicSystemPrompt(UserSceneSession session) { | ||
| 80 | + // 获取当前工具编号(用于 JSON 的 name 字段) | ||
| 81 | + String methodNo = session.getCurrentTool().getSMethodNo(); | ||
| 82 | + return """ | ||
| 83 | + 【极强约束·必须执行】 | ||
| 84 | + 1. 禁止说话!禁止解释! | ||
| 85 | + 2. 必须调用工具! | ||
| 86 | + 3. 只输出标准工具调用JSON! | ||
| 87 | + | ||
| 88 | + ### 任务背景 ### | ||
| 89 | + 用户会输入自然语言指令(如“第1行确认”、“全部合并”等)。 | ||
| 90 | + 你的任务是:解析出【操作类型】和【目标行号】。 | ||
| 91 | + | ||
| 92 | + ### 解析规则(必须严格遵守) ### | ||
| 93 | + | ||
| 94 | + 1. **操作类型 (operateType)**: | ||
| 95 | + - 如果用户说“全部确认”、“生成多个单据”,识别为:"全部确认" | ||
| 96 | + - 如果用户说“合并确认”、“生成一个单据”,识别为:"合并确认" | ||
| 97 | + - 其他情况,默认为:"单行确认" | ||
| 98 | + | ||
| 99 | + 2. **行号提取 (rowNumbers)**: | ||
| 100 | + - 单选:如“第5行”,提取为 [5] | ||
| 101 | + - 多选:如“第1、3、5行”,提取为 [1, 3, 5] | ||
| 102 | + - 范围:如“第2到4行”,提取为 [2, 3, 4] | ||
| 103 | + - 全选:如“全部”、“所有”,提取为 ["ALL"] | ||
| 104 | + | ||
| 105 | + ### 输出格式规范 ### | ||
| 106 | + 请直接输出以下 JSON(不要任何 Markdown 格式,不要 ```json): | ||
| 107 | + {"name": "%s", "parameters": {"operateType": "解析出的类型", "rowNumbers": [行号数组或"ALL"]}} | ||
| 108 | + | ||
| 109 | + """.formatted(methodNo); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | +} | ||
| 0 | \ No newline at end of file | 113 | \ No newline at end of file |
src/main/java/com/xly/config/BizExecuteUtil.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/BizExecuteUtil.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import groovy.lang.GroovyClassLoader; | ||
| 4 | +import groovy.lang.GroovyObject; | ||
| 5 | +import lombok.RequiredArgsConstructor; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.expression.Expression; | ||
| 8 | +import org.springframework.expression.ExpressionParser; | ||
| 9 | +import org.springframework.expression.spel.standard.SpelExpressionParser; | ||
| 10 | +import org.springframework.expression.spel.support.StandardEvaluationContext; | ||
| 11 | +import org.springframework.stereotype.Component; | ||
| 12 | + | ||
| 13 | +import java.util.Map; | ||
| 14 | +import java.util.Objects; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 业务逻辑执行工具:EL表达式 + Groovy脚本 | ||
| 18 | + */ | ||
| 19 | +@Slf4j | ||
| 20 | +@Component | ||
| 21 | +@RequiredArgsConstructor | ||
| 22 | +public class BizExecuteUtil { | ||
| 23 | + /** | ||
| 24 | + * EL表达式解析器 | ||
| 25 | + */ | ||
| 26 | + private final ExpressionParser elParser = new SpelExpressionParser(); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * Groovy类加载器 | ||
| 30 | + */ | ||
| 31 | + private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 执行业务逻辑 | ||
| 35 | + * @param bizType 1=EL,2=Groovy | ||
| 36 | + * @param bizContent 逻辑内容 | ||
| 37 | + * @param params JSON参数 | ||
| 38 | + * @return 执行结果 | ||
| 39 | + */ | ||
| 40 | + public String execute(Integer bizType, String bizContent, Map<String, Object> params) { | ||
| 41 | + if (Objects.isNull(bizType) || Objects.isNull(bizContent) || bizContent.isBlank()) { | ||
| 42 | + throw new IllegalArgumentException("业务逻辑配置异常"); | ||
| 43 | + } | ||
| 44 | + // EL表达式执行 | ||
| 45 | + if (1 == bizType) { | ||
| 46 | + return executeEL(bizContent, params); | ||
| 47 | + } | ||
| 48 | + // Groovy脚本执行 | ||
| 49 | + else if (2 == bizType) { | ||
| 50 | + return executeGroovy(bizContent, params); | ||
| 51 | + } | ||
| 52 | + else { | ||
| 53 | + throw new IllegalArgumentException("不支持的业务逻辑类型:" + bizType); | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 执行EL表达式 | ||
| 59 | + */ | ||
| 60 | + private String executeEL(String elContent, Map<String, Object> params) { | ||
| 61 | + try { | ||
| 62 | + StandardEvaluationContext context = new StandardEvaluationContext(); | ||
| 63 | + context.setVariable("params", params); | ||
| 64 | + Expression expression = elParser.parseExpression(elContent); | ||
| 65 | + return expression.getValue(context, String.class); | ||
| 66 | + } catch (Exception e) { | ||
| 67 | + log.error("EL表达式执行失败:{}", elContent, e); | ||
| 68 | + throw new IllegalArgumentException("EL表达式执行失败:" + e.getMessage()); | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 执行Groovy脚本 | ||
| 74 | + */ | ||
| 75 | + private String executeGroovy(String groovyContent, Map<String, Object> params) { | ||
| 76 | + try { | ||
| 77 | + // 构建Groovy脚本类 | ||
| 78 | + String scriptCode = "class DynamicScript { def execute(Map params) { " + groovyContent + " } }"; | ||
| 79 | + Class<?> groovyClass = groovyClassLoader.parseClass(scriptCode); | ||
| 80 | + GroovyObject groovyObject = (GroovyObject) groovyClass.getDeclaredConstructor().newInstance(); | ||
| 81 | + // 执行脚本并返回结果 | ||
| 82 | + Object result = groovyObject.invokeMethod("execute", new Object[]{params}); | ||
| 83 | + return Objects.isNull(result) ? "执行成功" : result.toString(); | ||
| 84 | + } catch (Exception e) { | ||
| 85 | + log.error("Groovy脚本执行失败:{}", groovyContent, e); | ||
| 86 | + throw new IllegalArgumentException("Groovy脚本执行失败:" + e.getMessage()); | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | +} | ||
| 0 | \ No newline at end of file | 90 | \ No newline at end of file |
src/main/java/com/xly/config/CorsConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/CorsConfig.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import org.springframework.context.annotation.Bean; | ||
| 4 | +import org.springframework.context.annotation.Configuration; | ||
| 5 | +import org.springframework.web.cors.CorsConfiguration; | ||
| 6 | +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||
| 7 | +import org.springframework.web.filter.CorsFilter; | ||
| 8 | +import org.springframework.web.servlet.config.annotation.CorsRegistry; | ||
| 9 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
| 10 | + | ||
| 11 | +/*** | ||
| 12 | + * @Author 钱豹 | ||
| 13 | + * @Date 22:40 2026/2/3 | ||
| 14 | + * @Param | ||
| 15 | + * @return | ||
| 16 | + * @Description 跨域配置 | ||
| 17 | + **/ | ||
| 18 | +@Configuration | ||
| 19 | +public class CorsConfig { | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 允许所有跨域请求 - CorsFilter方式 | ||
| 23 | + */ | ||
| 24 | + @Bean | ||
| 25 | + public CorsFilter corsFilter() { | ||
| 26 | + CorsConfiguration config = new CorsConfiguration(); | ||
| 27 | + | ||
| 28 | + // 允许所有域名 | ||
| 29 | + config.addAllowedOriginPattern("*"); | ||
| 30 | + | ||
| 31 | + // 允许所有请求方法 | ||
| 32 | + config.addAllowedMethod("*"); | ||
| 33 | + | ||
| 34 | + // 允许所有请求头 | ||
| 35 | + config.addAllowedHeader("*"); | ||
| 36 | + | ||
| 37 | + // 允许携带凭证(如cookies) | ||
| 38 | + config.setAllowCredentials(true); | ||
| 39 | + | ||
| 40 | + // 暴露所有响应头 | ||
| 41 | + config.addExposedHeader("*"); | ||
| 42 | + | ||
| 43 | + // 预检请求缓存时间 | ||
| 44 | + config.setMaxAge(3600L); | ||
| 45 | + | ||
| 46 | + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | ||
| 47 | + source.registerCorsConfiguration("/**", config); | ||
| 48 | + | ||
| 49 | + return new CorsFilter(source); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 允许所有跨域请求 - WebMvcConfigurer方式 | ||
| 54 | + */ | ||
| 55 | + @Bean | ||
| 56 | + public WebMvcConfigurer corsConfigurer() { | ||
| 57 | + return new WebMvcConfigurer() { | ||
| 58 | + @Override | ||
| 59 | + public void addCorsMappings(CorsRegistry registry) { | ||
| 60 | + registry.addMapping("/**") | ||
| 61 | + .allowedOriginPatterns("*") // 使用 allowedOriginPatterns 代替 allowedOrigins | ||
| 62 | + .allowedMethods("*") | ||
| 63 | + .allowedHeaders("*") | ||
| 64 | + .exposedHeaders("*") | ||
| 65 | + .allowCredentials(true) | ||
| 66 | + .maxAge(3600); | ||
| 67 | + } | ||
| 68 | + }; | ||
| 69 | + } | ||
| 70 | +} | ||
| 0 | \ No newline at end of file | 71 | \ No newline at end of file |
src/main/java/com/xly/config/JacksonConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/JacksonConfig.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | +import com.fasterxml.jackson.databind.SerializationFeature; | ||
| 5 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
| 6 | +import org.springframework.context.annotation.Bean; | ||
| 7 | +import org.springframework.context.annotation.Configuration; | ||
| 8 | +import org.springframework.context.annotation.Primary; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 全局 ObjectMapper 配置:支持 LocalDate 序列化/反序列化 | ||
| 12 | + */ | ||
| 13 | +@Configuration | ||
| 14 | +public class JacksonConfig { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 配置好的 ObjectMapper:注册 JavaTimeModule,支持 Java 8 时间类型 | ||
| 18 | + */ | ||
| 19 | + @Bean | ||
| 20 | + @Primary // 标记为默认实例,避免多 ObjectMapper 冲突 | ||
| 21 | + public ObjectMapper objectMapper() { | ||
| 22 | + ObjectMapper mapper = new ObjectMapper(); | ||
| 23 | + // 1. 核心:注册 JSR310 模块,处理 LocalDate/LocalDateTime | ||
| 24 | + mapper.registerModule(new JavaTimeModule()); | ||
| 25 | + // 2. 关闭时间戳序列化,LocalDate 以 "yyyy-MM-dd" 字符串形式存储 | ||
| 26 | + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | ||
| 27 | + // 3. 忽略未知字段(AI 返回多余字段时不报错) | ||
| 28 | + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | ||
| 29 | + return mapper; | ||
| 30 | + } | ||
| 31 | +} | ||
| 0 | \ No newline at end of file | 32 | \ No newline at end of file |
src/main/java/com/xly/config/ModelConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/ModelConfig.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | +import com.fasterxml.jackson.databind.SerializationFeature; | ||
| 5 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
| 6 | +import com.xly.agent.DynamicTableNl2SqlAiAgent; | ||
| 7 | +import com.xly.agent.SceneSelectorAiAgent; | ||
| 8 | +import dev.langchain4j.model.ollama.OllamaChatModel; | ||
| 9 | +import dev.langchain4j.model.ollama.OllamaStreamingChatModel; | ||
| 10 | +import dev.langchain4j.service.AiServices; | ||
| 11 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | ||
| 12 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
| 13 | +import org.springframework.beans.factory.annotation.Value; | ||
| 14 | +import org.springframework.context.annotation.Bean; | ||
| 15 | +import org.springframework.context.annotation.Configuration; | ||
| 16 | +import org.springframework.context.annotation.Primary; | ||
| 17 | + | ||
| 18 | +import java.time.Duration; | ||
| 19 | + | ||
| 20 | +@Configuration | ||
| 21 | +public class ModelConfig { | ||
| 22 | + | ||
| 23 | + @Value("${langchain4j.ollama.base-url}") | ||
| 24 | + private String chatModelUrl; | ||
| 25 | + | ||
| 26 | + @Value("${langchain4j.ollama.chat-model-name}") | ||
| 27 | + private String chatModelName; | ||
| 28 | + | ||
| 29 | + @Value("${langchain4j.ollama.sql-model-name}") | ||
| 30 | + private String sqlModelName; | ||
| 31 | + | ||
| 32 | + // ====================== 主对话模型 ====================== | ||
| 33 | + @Bean | ||
| 34 | + @Primary | ||
| 35 | + public OllamaChatModel chatLanguageModel() { | ||
| 36 | + return OllamaChatModel.builder() | ||
| 37 | + .baseUrl(chatModelUrl) | ||
| 38 | + .modelName(chatModelName) | ||
| 39 | + .temperature(0.1) | ||
| 40 | + .topP(0.95) | ||
| 41 | + .timeout(Duration.ofSeconds(120)) | ||
| 42 | + .maxRetries(2) | ||
| 43 | + .logRequests(true) | ||
| 44 | + .logResponses(true) | ||
| 45 | + .build(); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + // ====================== 自由闲聊模型 ====================== | ||
| 49 | + @Bean("chatiModel") | ||
| 50 | + public OllamaChatModel chatiModel() { | ||
| 51 | + return OllamaChatModel.builder() | ||
| 52 | + .baseUrl(chatModelUrl) | ||
| 53 | + .modelName(chatModelName) | ||
| 54 | + .temperature(0.7) | ||
| 55 | + .topP(0.9) | ||
| 56 | + .timeout(Duration.ofSeconds(60)) | ||
| 57 | + .maxRetries(2) | ||
| 58 | + .build(); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + // ====================== SQL 专用模型 ====================== | ||
| 62 | + @Bean("sqlChatModel") | ||
| 63 | + public OllamaChatModel sqlChatModel() { | ||
| 64 | + return OllamaChatModel.builder() | ||
| 65 | + .baseUrl(chatModelUrl) | ||
| 66 | + .modelName(sqlModelName) | ||
| 67 | + .temperature(0.0) | ||
| 68 | + .topP(0.95) | ||
| 69 | + .numPredict(4096) | ||
| 70 | + .timeout(Duration.ofSeconds(120)) | ||
| 71 | + .maxRetries(3) | ||
| 72 | + .build(); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + // ====================== 流式对话模型 ====================== | ||
| 76 | + @Bean("streamingChatModel") | ||
| 77 | + @Primary | ||
| 78 | + public OllamaStreamingChatModel streamingChatModel() { | ||
| 79 | + return OllamaStreamingChatModel.builder() | ||
| 80 | + .baseUrl(chatModelUrl) | ||
| 81 | + .modelName(chatModelName) | ||
| 82 | + .temperature(0.3) | ||
| 83 | + .topP(0.9) | ||
| 84 | + .numPredict(1024) | ||
| 85 | + .timeout(Duration.ofSeconds(60)) | ||
| 86 | + .build(); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // ====================== 流式 SQL 模型 ====================== | ||
| 90 | + @Bean("streamingSqlModel") | ||
| 91 | + public OllamaStreamingChatModel streamingSqlModel() { | ||
| 92 | + return OllamaStreamingChatModel.builder() | ||
| 93 | + .baseUrl(chatModelUrl) | ||
| 94 | + .modelName(sqlModelName) | ||
| 95 | + .temperature(0.2) | ||
| 96 | + .topP(0.95) | ||
| 97 | + .numPredict(2048) | ||
| 98 | + .timeout(Duration.ofSeconds(120)) | ||
| 99 | + .build(); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // ====================== JSON ====================== | ||
| 103 | + @Bean | ||
| 104 | + @Primary | ||
| 105 | + public ObjectMapper objectMapper() { | ||
| 106 | + ObjectMapper mapper = new ObjectMapper(); | ||
| 107 | + mapper.registerModule(new JavaTimeModule()); | ||
| 108 | + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | ||
| 109 | + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | ||
| 110 | + return mapper; | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + // ====================== 动态 SQL Agent ====================== | ||
| 114 | + @Bean | ||
| 115 | + public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( | ||
| 116 | + @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { | ||
| 117 | + return AiServices.builder(DynamicTableNl2SqlAiAgent.class) | ||
| 118 | + .chatModel(sqlModel) | ||
| 119 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) | ||
| 120 | + .build(); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + // ====================== 场景选择 Agent ====================== | ||
| 124 | + @Bean | ||
| 125 | + public SceneSelectorAiAgent sceneSelectorAiAgent( | ||
| 126 | + @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { | ||
| 127 | + return AiServices.builder(SceneSelectorAiAgent.class) | ||
| 128 | + .chatModel(chatLanguageModel) | ||
| 129 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) | ||
| 130 | + .build(); | ||
| 131 | + } | ||
| 132 | +} | ||
| 0 | \ No newline at end of file | 133 | \ No newline at end of file |
src/main/java/com/xly/config/MvcConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/MvcConfig.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import org.springframework.context.annotation.Configuration; | ||
| 4 | +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; | ||
| 5 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
| 6 | + | ||
| 7 | +@Configuration | ||
| 8 | +public class MvcConfig implements WebMvcConfigurer { | ||
| 9 | + | ||
| 10 | + @Override | ||
| 11 | + public void addViewControllers(ViewControllerRegistry registry) { | ||
| 12 | + // 直接映射URL到视图 | ||
| 13 | + registry.addViewController("/chat").setViewName("chat"); | ||
| 14 | + registry.addViewController("/").setViewName("index"); | ||
| 15 | + } | ||
| 16 | +} | ||
| 0 | \ No newline at end of file | 17 | \ No newline at end of file |
src/main/java/com/xly/config/OperableChatMemoryProvider.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/OperableChatMemoryProvider.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.memory.ChatMemory; | ||
| 4 | +import dev.langchain4j.memory.chat.ChatMemoryProvider; | ||
| 5 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | ||
| 6 | +import dev.langchain4j.data.message.ChatMessage; | ||
| 7 | +import org.springframework.stereotype.Component; | ||
| 8 | + | ||
| 9 | +import java.util.ArrayList; | ||
| 10 | +import java.util.List; | ||
| 11 | +import java.util.Map; | ||
| 12 | +import java.util.Objects; | ||
| 13 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 14 | +import java.util.stream.Collectors; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 可操作的ChatMemoryProvider:获取消息对象+清除记忆(指定/全量)+删除单条消息 | ||
| 18 | + * 实现框架原生接口,无缝对接AiServices,线程安全适配生产环境 | ||
| 19 | + */ | ||
| 20 | +@Component | ||
| 21 | +public class OperableChatMemoryProvider implements ChatMemoryProvider { | ||
| 22 | + // 核心缓存:memoryId -> ChatMemory,保证一个会话/用户对应唯一记忆实例 | ||
| 23 | + private final Map<Object, ChatMemory> memoryCache = new ConcurrentHashMap<>(); | ||
| 24 | + // 记忆最大消息数,根据业务需求调整 | ||
| 25 | + private static final int MAX_MESSAGE_SIZE = 100; | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 框架原生方法:获取【当前memoryId对应的ChatMemory实例(含消息对象)】 | ||
| 29 | + * AiServices自动调用,也是手动操作记忆/消息的唯一入口 | ||
| 30 | + */ | ||
| 31 | + @Override | ||
| 32 | + public ChatMemory get(Object memoryId) { | ||
| 33 | + // 空memoryId兜底,避免空指针 | ||
| 34 | + Object finalMemId = Objects.isNull(memoryId) ? "default_erp_chat_memory" : memoryId; | ||
| 35 | + // 不存在则创建MessageWindowChatMemory,存在则复用 | ||
| 36 | + return memoryCache.computeIfAbsent(finalMemId, k -> MessageWindowChatMemory.withMaxMessages(MAX_MESSAGE_SIZE)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + // ===================== 1. 获取消息对象 ===================== | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 获取当前会话/用户的全部消息列表 | ||
| 43 | + */ | ||
| 44 | + public List<ChatMessage> getCurrentChatMessages(Object memoryId) { | ||
| 45 | + if (Objects.isNull(memoryId)) { | ||
| 46 | + return new ArrayList<>(); | ||
| 47 | + } | ||
| 48 | + // 返回一个新的列表,避免直接操作内部列表 | ||
| 49 | + return new ArrayList<>(this.get(memoryId).messages()); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + // ===================== 2. 清除记忆 ===================== | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 清空【指定memoryId】的全部记忆 | ||
| 56 | + */ | ||
| 57 | + public void clearSpecifiedMemory(Object memoryId) { | ||
| 58 | + if (Objects.nonNull(memoryId)) { | ||
| 59 | + this.get(memoryId).clear(); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 全量清除【所有memoryId】的记忆 | ||
| 65 | + */ | ||
| 66 | + public void clearAllMemory() { | ||
| 67 | + memoryCache.values().forEach(ChatMemory::clear); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + // ===================== 3. 删除单条消息(完全重新设置方案) ===================== | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 删除指定消息(通过完全重新设置消息列表的方式) | ||
| 74 | + * @param memoryId 会话ID | ||
| 75 | + * @param messageToDelete 要删除的消息对象 | ||
| 76 | + * @return 删除后的最新消息列表 | ||
| 77 | + */ | ||
| 78 | + public List<ChatMessage> deleteSingleMessage(Object memoryId, ChatMessage messageToDelete) { | ||
| 79 | + if (Objects.isNull(memoryId) || Objects.isNull(messageToDelete)) { | ||
| 80 | + return getCurrentChatMessages(memoryId); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + // 步骤1: 获取当前所有消息 | ||
| 84 | + ChatMemory currentMemory = this.get(memoryId); | ||
| 85 | + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages()); | ||
| 86 | + | ||
| 87 | + // 从后往前查找内容匹配的最后一条消息 | ||
| 88 | + int indexToDelete = -1; | ||
| 89 | + for (int i = currentMessages.size() - 1; i >= 0; i--) { | ||
| 90 | + ChatMessage msg = currentMessages.get(i); | ||
| 91 | + if (isSameMessage(msg, messageToDelete)) { | ||
| 92 | + indexToDelete = i; | ||
| 93 | + break; | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + // 如果找到匹配的消息 | ||
| 97 | + if (indexToDelete >= 0) { | ||
| 98 | + List<ChatMessage> filteredMessages = new ArrayList<>(currentMessages); | ||
| 99 | + filteredMessages.remove(indexToDelete); | ||
| 100 | + return rebuildMemoryWithMessages(memoryId, filteredMessages); | ||
| 101 | + } | ||
| 102 | + // 步骤4: 完全重新设置消息列表 | ||
| 103 | + return rebuildMemoryWithMessages(memoryId, currentMessages); | ||
| 104 | + } | ||
| 105 | + public List<ChatMessage> deleteUserLasterMessage(Object memoryId) { | ||
| 106 | + if (Objects.isNull(memoryId)) { | ||
| 107 | + return getCurrentChatMessages(memoryId); | ||
| 108 | + } | ||
| 109 | + // 步骤1: 获取当前所有消息 | ||
| 110 | + ChatMemory currentMemory = this.get(memoryId); | ||
| 111 | + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages()); | ||
| 112 | + | ||
| 113 | + // 从后往前查找内容匹配的最后一条消息 | ||
| 114 | + int indexToDelete = currentMessages.size()-1; | ||
| 115 | + // 如果找到匹配的消息 | ||
| 116 | + if (indexToDelete >= 0) { | ||
| 117 | + List<ChatMessage> filteredMessages = new ArrayList<>(currentMessages); | ||
| 118 | + filteredMessages.remove(indexToDelete); | ||
| 119 | + return rebuildMemoryWithMessages(memoryId, filteredMessages); | ||
| 120 | + } | ||
| 121 | + // 步骤4: 完全重新设置消息列表 | ||
| 122 | + return rebuildMemoryWithMessages(memoryId, currentMessages); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + public List<ChatMessage> deleteUserLasterMessageBySize(Object memoryId,Integer size) { | ||
| 126 | + if (Objects.isNull(memoryId) || size==0) { | ||
| 127 | + return getCurrentChatMessages(memoryId); | ||
| 128 | + } | ||
| 129 | + // 步骤1: 获取当前所有消息 | ||
| 130 | + ChatMemory currentMemory = this.get(memoryId); | ||
| 131 | + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages()); | ||
| 132 | + // 从后往前查找内容匹配的最后一条消息 | ||
| 133 | + int indexToDelete = currentMessages.size(); | ||
| 134 | + // 如果找到匹配的消息 | ||
| 135 | + if (indexToDelete >= 0) { | ||
| 136 | + List<ChatMessage> filteredMessages = new ArrayList<>(currentMessages); | ||
| 137 | + for(int i=0;i<size;i++){ | ||
| 138 | + indexToDelete = indexToDelete -1; | ||
| 139 | + if(indexToDelete>1){ | ||
| 140 | + filteredMessages.remove(indexToDelete); | ||
| 141 | + } | ||
| 142 | + } | ||
| 143 | + return rebuildMemoryWithMessages(memoryId, filteredMessages); | ||
| 144 | + } | ||
| 145 | + // 步骤4: 完全重新设置消息列表 | ||
| 146 | + return rebuildMemoryWithMessages(memoryId, currentMessages); | ||
| 147 | + } | ||
| 148 | + /** | ||
| 149 | + * 批量删除多条消息 | ||
| 150 | + * @param memoryId 会话ID | ||
| 151 | + * @param messagesToDelete 要删除的消息列表 | ||
| 152 | + * @return 删除后的最新消息列表 | ||
| 153 | + */ | ||
| 154 | + public List<ChatMessage> deleteMessages(Object memoryId, List<ChatMessage> messagesToDelete) { | ||
| 155 | + if (Objects.isNull(memoryId) || messagesToDelete == null || messagesToDelete.isEmpty()) { | ||
| 156 | + return getCurrentChatMessages(memoryId); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + // 获取当前所有消息 | ||
| 160 | + ChatMemory currentMemory = this.get(memoryId); | ||
| 161 | + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages()); | ||
| 162 | + | ||
| 163 | + // 过滤掉所有要删除的消息 | ||
| 164 | + List<ChatMessage> filteredMessages = currentMessages.stream() | ||
| 165 | + .filter(msg -> messagesToDelete.stream().noneMatch(delMsg -> isSameMessage(msg, delMsg))) | ||
| 166 | + .collect(Collectors.toList()); | ||
| 167 | + | ||
| 168 | + // 如果消息数量没有变化,直接返回 | ||
| 169 | + if (filteredMessages.size() == currentMessages.size()) { | ||
| 170 | + return currentMessages; | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + // 重新设置消息列表 | ||
| 174 | + return rebuildMemoryWithMessages(memoryId, filteredMessages); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /** | ||
| 178 | + * 判断两条消息是否相同(支持根据内容、类型等多维度匹配) | ||
| 179 | + */ | ||
| 180 | + private boolean isSameMessage(ChatMessage msg1, ChatMessage msg2) { | ||
| 181 | +// if (msg1 == msg2) return true; // 同一对象引用 | ||
| 182 | +// if (msg1 == null || msg2 == null) return false; | ||
| 183 | +// | ||
| 184 | +// // 根据消息类型和内容判断 | ||
| 185 | +// // 方式1: 根据对象相等性 | ||
| 186 | +// if (msg1.equals(msg2)) return true; | ||
| 187 | + // 方式2: 根据消息类型和文本内容(适用于大多数场景) | ||
| 188 | + if (msg1.type() == msg2.type()) { | ||
| 189 | + // 如果有唯一ID或时间戳,也可以加入判断 | ||
| 190 | + // 这里简单使用文本内容判断 | ||
| 191 | + String text1 = extractText(msg1); | ||
| 192 | + String text2 = extractText(msg2); | ||
| 193 | + return Objects.equals(text1, text2); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + return false; | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + /** | ||
| 200 | + * 从消息中提取文本内容 | ||
| 201 | + */ | ||
| 202 | + private String extractText(ChatMessage message) { | ||
| 203 | + // 根据ChatMessage的实际类型提取文本 | ||
| 204 | + // 这里需要根据您的具体消息实现来调整 | ||
| 205 | + try { | ||
| 206 | + // 尝试通过toString获取内容 | ||
| 207 | + return message.toString(); | ||
| 208 | + } catch (Exception e) { | ||
| 209 | + return ""; | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + /** | ||
| 214 | + * 核心方法:使用过滤后的消息列表重建记忆 | ||
| 215 | + */ | ||
| 216 | + private List<ChatMessage> rebuildMemoryWithMessages(Object memoryId, List<ChatMessage> messages) { | ||
| 217 | + // 从缓存中移除原实例 | ||
| 218 | + ChatMemory oldMemory = memoryCache.remove(memoryId); | ||
| 219 | + | ||
| 220 | + // 创建新的ChatMemory实例 | ||
| 221 | + MessageWindowChatMemory newMemory = MessageWindowChatMemory.withMaxMessages(MAX_MESSAGE_SIZE); | ||
| 222 | + | ||
| 223 | + // 由于ChatMemory接口没有直接添加消息的方法,我们需要通过特定方式添加 | ||
| 224 | + // 方式1: 如果MessageWindowChatMemory有add方法(需要通过反射或强制类型转换) | ||
| 225 | + try { | ||
| 226 | + // 通过反射调用add方法(如果存在) | ||
| 227 | + java.lang.reflect.Method addMethod = newMemory.getClass().getMethod("add", ChatMessage.class); | ||
| 228 | + addMethod.setAccessible(true); | ||
| 229 | + for (ChatMessage message : messages) { | ||
| 230 | + addMethod.invoke(newMemory, message); | ||
| 231 | + } | ||
| 232 | + } catch (Exception e) { | ||
| 233 | + // 方式2: 如果无法通过反射添加,我们需要使用替代方案 | ||
| 234 | + // 这里可以记录日志 | ||
| 235 | + System.err.println("无法通过反射添加消息: " + e.getMessage()); | ||
| 236 | + | ||
| 237 | + // 方式3: 使用clear后通过某种方式重新添加 | ||
| 238 | + // 注意:这取决于具体的ChatMemory实现 | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + // 将新实例放入缓存 | ||
| 242 | + memoryCache.put(memoryId, newMemory); | ||
| 243 | + | ||
| 244 | + // 返回重建后的消息列表 | ||
| 245 | + return new ArrayList<>(newMemory.messages()); | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + /** | ||
| 249 | + * 移除并清除指定记忆(清空消息+从缓存删除实例) | ||
| 250 | + */ | ||
| 251 | + public void removeAndClearMemory(Object memoryId) { | ||
| 252 | + if (Objects.nonNull(memoryId)) { | ||
| 253 | + ChatMemory chatMemory = memoryCache.remove(memoryId); | ||
| 254 | + if (Objects.nonNull(chatMemory)) { | ||
| 255 | + chatMemory.clear(); | ||
| 256 | + } | ||
| 257 | + } | ||
| 258 | + } | ||
| 259 | +} | ||
| 0 | \ No newline at end of file | 260 | \ No newline at end of file |
src/main/java/com/xly/config/RedisConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/RedisConfig.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import org.springframework.context.annotation.Bean; | ||
| 4 | +import org.springframework.context.annotation.Configuration; | ||
| 5 | +import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
| 6 | +import org.springframework.data.redis.core.RedisTemplate; | ||
| 7 | +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; | ||
| 8 | +import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
| 9 | + | ||
| 10 | +@Configuration | ||
| 11 | +public class RedisConfig { | ||
| 12 | + | ||
| 13 | + @Bean | ||
| 14 | + public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { | ||
| 15 | + RedisTemplate<String, Object> template = new RedisTemplate<>(); | ||
| 16 | + template.setConnectionFactory(factory); | ||
| 17 | + | ||
| 18 | + // key 采用 String 序列化 | ||
| 19 | + template.setKeySerializer(new StringRedisSerializer()); | ||
| 20 | + // hash 的 key 也采用 String 序列化 | ||
| 21 | + template.setHashKeySerializer(new StringRedisSerializer()); | ||
| 22 | + | ||
| 23 | + // value 采用 Jackson 序列化 | ||
| 24 | + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); | ||
| 25 | + template.setValueSerializer(jsonSerializer); | ||
| 26 | + template.setHashValueSerializer(jsonSerializer); | ||
| 27 | + | ||
| 28 | + template.afterPropertiesSet(); | ||
| 29 | + return template; | ||
| 30 | + } | ||
| 31 | +} | ||
| 0 | \ No newline at end of file | 32 | \ No newline at end of file |
src/main/java/com/xly/config/SpringContextHolder.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/config/SpringContextHolder.java | ||
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.lang3.Validate; | ||
| 4 | +import org.slf4j.Logger; | ||
| 5 | +import org.slf4j.LoggerFactory; | ||
| 6 | +import org.springframework.beans.factory.DisposableBean; | ||
| 7 | +import org.springframework.context.ApplicationContext; | ||
| 8 | +import org.springframework.context.ApplicationContextAware; | ||
| 9 | +import org.springframework.context.annotation.Lazy; | ||
| 10 | +import org.springframework.stereotype.Service; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext. | ||
| 14 | + */ | ||
| 15 | +@Service | ||
| 16 | +@Lazy(false) | ||
| 17 | +public class SpringContextHolder implements ApplicationContextAware, DisposableBean { | ||
| 18 | + | ||
| 19 | + private static ApplicationContext applicationContext = null; | ||
| 20 | + | ||
| 21 | + private static Logger logger = LoggerFactory.getLogger(SpringContextHolder.class); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 取得存储在静态变量中的ApplicationContext. | ||
| 25 | + */ | ||
| 26 | + public static ApplicationContext getApplicationContext() { | ||
| 27 | + assertContextInjected(); | ||
| 28 | + return applicationContext; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. | ||
| 33 | + */ | ||
| 34 | + @SuppressWarnings("unchecked") | ||
| 35 | + public static <T> T getBean(String name) { | ||
| 36 | + assertContextInjected(); | ||
| 37 | + return (T) applicationContext.getBean(name); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. | ||
| 42 | + */ | ||
| 43 | + public static <T> T getBean(Class<T> requiredType) { | ||
| 44 | + assertContextInjected(); | ||
| 45 | + return applicationContext.getBean(requiredType); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 清除SpringContextHolder中的ApplicationContext为Null. | ||
| 50 | + */ | ||
| 51 | + public static void clearHolder() { | ||
| 52 | + if (logger.isDebugEnabled()){ | ||
| 53 | + logger.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); | ||
| 54 | + } | ||
| 55 | + applicationContext = null; | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 实现ApplicationContextAware接口, 注入Context到静态变量中. | ||
| 60 | + */ | ||
| 61 | + @Override | ||
| 62 | + public void setApplicationContext(ApplicationContext applicationContext) { | ||
| 63 | + SpringContextHolder.applicationContext = applicationContext; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 实现DisposableBean接口, 在Context关闭时清理静态变量. | ||
| 68 | + */ | ||
| 69 | + @Override | ||
| 70 | + public void destroy() throws Exception { | ||
| 71 | + SpringContextHolder.clearHolder(); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 检查ApplicationContext不为空. | ||
| 76 | + */ | ||
| 77 | + private static void assertContextInjected() { | ||
| 78 | + Validate.validState(applicationContext != null, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder."); | ||
| 79 | + } | ||
| 80 | +} | ||
| 0 | \ No newline at end of file | 81 | \ No newline at end of file |
src/main/java/com/xly/constant/BusinessCode.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/BusinessCode.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +import lombok.Getter; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 9:53 2026/2/9 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description //TODO | ||
| 11 | + **/ | ||
| 12 | +@Getter | ||
| 13 | +public enum BusinessCode { | ||
| 14 | + | ||
| 15 | + //报价确认 | ||
| 16 | + QUOCONFIRM("quoconfirm", "报价确认"), | ||
| 17 | + COMMONTS("commonTs", "如果切换场景,点[回首页],如果在本场景下,转换意图,点[清除记忆]"); | ||
| 18 | + | ||
| 19 | + private final String code; | ||
| 20 | + private final String message; | ||
| 21 | + | ||
| 22 | + BusinessCode(String code, String message) { | ||
| 23 | + this.code = code; | ||
| 24 | + this.message = message; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 根据code获取ErrorCode | ||
| 29 | + */ | ||
| 30 | + public static BusinessCode getByCode(Integer code) { | ||
| 31 | + for (BusinessCode errorCode : values()) { | ||
| 32 | + if (errorCode.getCode().equals(code)) { | ||
| 33 | + return errorCode; | ||
| 34 | + } | ||
| 35 | + } | ||
| 36 | + return QUOCONFIRM; | ||
| 37 | + } | ||
| 38 | +} | ||
| 0 | \ No newline at end of file | 39 | \ No newline at end of file |
src/main/java/com/xly/constant/CommonConstant.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/CommonConstant.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 全局通用常量 | ||
| 5 | + * 包含大模型解析提示语、请求头、资源前缀等 | ||
| 6 | + */ | ||
| 7 | +public class CommonConstant { | ||
| 8 | + //重置方法 | ||
| 9 | + public static final String RESET = "<button \n" + | ||
| 10 | + " data-action=\"reset\" \n" + | ||
| 11 | + " style=\"\n" + | ||
| 12 | + " background: linear-gradient(135deg, #1677ff, #4096ff);\n" + | ||
| 13 | + " color: white;\n" + | ||
| 14 | + " border: none;\n" + | ||
| 15 | + " padding: 6px 14px;\n" + | ||
| 16 | + " border-radius: 6px;\n" + | ||
| 17 | + " cursor: pointer;\n" + | ||
| 18 | + " font-size: 12px;\n" + | ||
| 19 | + " font-weight: 500;\n" + | ||
| 20 | + " transition: all 0.3s ease;\n" + | ||
| 21 | + " \"\n" + | ||
| 22 | + " data-text=\"重置\"\n" + | ||
| 23 | + " onclick=\"reset('重置')\"\n" + | ||
| 24 | + " onmouseover=\"this.style.opacity='0.8'\"\n" + | ||
| 25 | + " onmouseout=\"this.style.opacity='1'\">\n" + | ||
| 26 | + " 重置\n" + | ||
| 27 | + "</button> 复位 \n\n"; | ||
| 28 | + | ||
| 29 | +} |
src/main/java/com/xly/constant/ErrorCode.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/ErrorCode.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +import lombok.Getter; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:04 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 异常码枚举 | ||
| 11 | + **/ | ||
| 12 | +@Getter | ||
| 13 | +public enum ErrorCode { | ||
| 14 | + | ||
| 15 | + // 成功 | ||
| 16 | + SUCCESS(200, "操作成功"), | ||
| 17 | + SUCCESSMSG(201, "成功"), | ||
| 18 | + ERRORMSG(202, "失败"), | ||
| 19 | + WFHYY(203, "未返回原因"), | ||
| 20 | + | ||
| 21 | + // 客户端错误 | ||
| 22 | + BAD_REQUEST(400, "请求参数错误"), | ||
| 23 | + UNAUTHORIZED(401, "未授权"), | ||
| 24 | + FORBIDDEN(403, "禁止访问"), | ||
| 25 | + NOT_FOUND(404, "资源不存在"), | ||
| 26 | + | ||
| 27 | + // 参数错误 | ||
| 28 | + PARAM_ERROR(40001, "参数错误"), | ||
| 29 | + PARAM_REQUIRED(40002, "参数缺失"), | ||
| 30 | + PARAM_TYPE_ERROR(40003, "参数类型错误"), | ||
| 31 | + PARAM_FORMAT_ERROR(40004, "参数格式错误"), | ||
| 32 | + | ||
| 33 | + // 业务错误 | ||
| 34 | + BUSINESS_ERROR(50001, "业务异常"), | ||
| 35 | + DATA_ERROR(50002, "数据异常"), | ||
| 36 | + DATA_NOT_FOUND(50003, "数据不存在"), | ||
| 37 | + DATA_EXISTS(50004, "数据已存在"), | ||
| 38 | + DATA_STATE_ERROR(50005, "数据状态异常"), | ||
| 39 | + | ||
| 40 | + // 用户相关 | ||
| 41 | + USER_NOT_FOUND(60001, "用户不存在"), | ||
| 42 | + USER_DISABLED(60002, "用户已禁用"), | ||
| 43 | + USER_PASSWORD_ERROR(60003, "密码错误"), | ||
| 44 | + USER_NOT_LOGIN(60004, "用户未登录"), | ||
| 45 | + | ||
| 46 | + // 权限相关 | ||
| 47 | + PERMISSION_DENIED(70001, "权限不足"), | ||
| 48 | + ROLE_NOT_FOUND(70002, "角色不存在"), | ||
| 49 | + | ||
| 50 | + // 系统错误 | ||
| 51 | + SYSTEM_ERROR(10000, "系统异常"), | ||
| 52 | + SERVICE_UNAVAILABLE(10001, "服务不可用"), | ||
| 53 | + DB_ERROR(10002, "数据库异常"), | ||
| 54 | + NETWORK_ERROR(10003, "网络异常"), | ||
| 55 | + THIRD_PARTY_ERROR(10004, "第三方服务异常"), | ||
| 56 | + CONFIG_ERROR(10005, "配置错误"), | ||
| 57 | + | ||
| 58 | + // 文件相关 | ||
| 59 | + FILE_UPLOAD_ERROR(80001, "文件上传失败"), | ||
| 60 | + FILE_NOT_FOUND(80002, "文件不存在"), | ||
| 61 | + FILE_TYPE_ERROR(80003, "文件类型错误"), | ||
| 62 | + FILE_SIZE_ERROR(80004, "文件大小超限"), | ||
| 63 | + | ||
| 64 | + PYTHON_ERROR(9001, "Python脚本执行失败"); | ||
| 65 | + | ||
| 66 | + private final Integer code; | ||
| 67 | + private final String message; | ||
| 68 | + | ||
| 69 | + ErrorCode(Integer code, String message) { | ||
| 70 | + this.code = code; | ||
| 71 | + this.message = message; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 根据code获取ErrorCode | ||
| 76 | + */ | ||
| 77 | + public static ErrorCode getByCode(Integer code) { | ||
| 78 | + for (ErrorCode errorCode : values()) { | ||
| 79 | + if (errorCode.getCode().equals(code)) { | ||
| 80 | + return errorCode; | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | + return SYSTEM_ERROR; | ||
| 84 | + } | ||
| 85 | +} | ||
| 0 | \ No newline at end of file | 86 | \ No newline at end of file |
src/main/java/com/xly/constant/ProcedureConstant.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/ProcedureConstant.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +import java.util.ArrayList; | ||
| 4 | +import java.util.HashMap; | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 8 | +/*** | ||
| 9 | + * @Author 钱豹 | ||
| 10 | + * @Date 22:41 2026/2/3 | ||
| 11 | + * @Param | ||
| 12 | + * @return | ||
| 13 | + * @Description 调用过程的常量 | ||
| 14 | + **/ | ||
| 15 | +public class ProcedureConstant { | ||
| 16 | + | ||
| 17 | + public static final String PROTYPESTRING = "proc"; | ||
| 18 | + public static final String CONFTYPESTRING = "sType"; | ||
| 19 | + public static final String SSQLSTRSTRING = "sSqlStr"; | ||
| 20 | + public static final String SRETURN = "sReturn"; | ||
| 21 | + public static final String SCODE = "sCode"; | ||
| 22 | + public static final String OUTSETSTRING = "outSet"; | ||
| 23 | + public static final String SDEFAULT = "sDefault"; | ||
| 24 | + public static final String IN = "IN"; | ||
| 25 | + public static final String OUT = "OUT"; | ||
| 26 | + public static final String HEADER = "HEADER"; | ||
| 27 | + public static final String OUTLIST = "outList"; | ||
| 28 | + public static final String OUTMAP = "outMap"; | ||
| 29 | + | ||
| 30 | + public static Map<String, Object> getRetMap(List<Map<String, Object>> proList, Map<String, Object> outMap) { | ||
| 31 | + Map<String, Object> retMap = new HashMap<>(8); | ||
| 32 | + Map<String, Object> proMap = new HashMap<>(4); | ||
| 33 | + List<Map<String, Object>> outList = new ArrayList<>(1); | ||
| 34 | + outList.add(outMap); | ||
| 35 | + proMap.put("proData", proList); | ||
| 36 | + proMap.put("outData", outList); | ||
| 37 | + retMap.put("dataSet", proMap); | ||
| 38 | + retMap.put(SCODE, outMap.get(SCODE)); | ||
| 39 | + retMap.put(SRETURN, outMap.get(SRETURN)); | ||
| 40 | + return retMap; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | +} |
src/main/java/com/xly/constant/ReturnTypeCode.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/ReturnTypeCode.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +import lombok.Getter; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:04 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 异常码枚举 | ||
| 11 | + **/ | ||
| 12 | +@Getter | ||
| 13 | +public enum ReturnTypeCode { | ||
| 14 | + | ||
| 15 | + // 成功 | ||
| 16 | + HTML("html", "html"), | ||
| 17 | + JSON("JSON", "JSON"), | ||
| 18 | + STREAM("stream", "stream"), | ||
| 19 | + MAKEDOWN("makedown", "makedown"); | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + private final String code; | ||
| 23 | + private final String message; | ||
| 24 | + | ||
| 25 | + ReturnTypeCode(String code, String message) { | ||
| 26 | + this.code = code; | ||
| 27 | + this.message = message; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 根据code获取ErrorCode | ||
| 32 | + */ | ||
| 33 | + public static ReturnTypeCode getByCode(String code) { | ||
| 34 | + for (ReturnTypeCode errorCode : values()) { | ||
| 35 | + if (errorCode.getCode().equals(code)) { | ||
| 36 | + return errorCode; | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + return MAKEDOWN; | ||
| 40 | + } | ||
| 41 | +} | ||
| 0 | \ No newline at end of file | 42 | \ No newline at end of file |
src/main/java/com/xly/constant/RuleCode.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/RuleCode.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +import lombok.Getter; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:04 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 异常码枚举 | ||
| 11 | + **/ | ||
| 12 | +@Getter | ||
| 13 | +public enum RuleCode { | ||
| 14 | +// {"sql":"SQL","pro":"过程","const":"常量"} | ||
| 15 | + // 成功 | ||
| 16 | + SQL("sql", "SQL"), | ||
| 17 | + // 客户端错误 | ||
| 18 | + PRO("pro", "过程"), | ||
| 19 | + CONST("const", "常量"); | ||
| 20 | + | ||
| 21 | + private final String code; | ||
| 22 | + private final String message; | ||
| 23 | + | ||
| 24 | + RuleCode(String code, String message) { | ||
| 25 | + this.code = code; | ||
| 26 | + this.message = message; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 根据code获取ErrorCode | ||
| 31 | + */ | ||
| 32 | + public static RuleCode getByCode(Integer code) { | ||
| 33 | + for (RuleCode errorCode : values()) { | ||
| 34 | + if (errorCode.getCode().equals(code)) { | ||
| 35 | + return errorCode; | ||
| 36 | + } | ||
| 37 | + } | ||
| 38 | + return CONST; | ||
| 39 | + } | ||
| 40 | +} | ||
| 0 | \ No newline at end of file | 41 | \ No newline at end of file |
src/main/java/com/xly/constant/UrlErpConstant.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/constant/UrlErpConstant.java | ||
| 1 | +package com.xly.constant; | ||
| 2 | + | ||
| 3 | +/*** | ||
| 4 | + * @Author 钱豹 | ||
| 5 | + * @Date 0:37 2026/2/6 | ||
| 6 | + * @Param | ||
| 7 | + * @return | ||
| 8 | + * @Description ERP URL后缀 | ||
| 9 | + **/ | ||
| 10 | +public class UrlErpConstant { | ||
| 11 | + | ||
| 12 | + public static final String getBusinessDataByFormcustomId = "/business/getBusinessDataByFormcustomId/{}?sModelsId={}&sName="; | ||
| 13 | + | ||
| 14 | +} |
src/main/java/com/xly/entity/AiResponseAccumulator.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/AiResponseAccumulator.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import com.xly.entity.AiResponseDTO; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * AI响应累积器 | ||
| 9 | + */ | ||
| 10 | +@Slf4j | ||
| 11 | +public class AiResponseAccumulator { | ||
| 12 | + | ||
| 13 | + private final String requestId; | ||
| 14 | + private final StringBuilder aiTextBuilder = new StringBuilder(); | ||
| 15 | + private final StringBuilder systemTextBuilder = new StringBuilder(); | ||
| 16 | + private String sSceneName; | ||
| 17 | + private String sMethodName; | ||
| 18 | + private String sReturnType; | ||
| 19 | + private int totalChunks = 0; | ||
| 20 | + private int processedChunks = 0; | ||
| 21 | + private final long startTime; | ||
| 22 | + | ||
| 23 | + public AiResponseAccumulator(String requestId) { | ||
| 24 | + this.requestId = requestId; | ||
| 25 | + this.startTime = System.currentTimeMillis(); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 累积单个AI响应 | ||
| 30 | + */ | ||
| 31 | + public void accumulate(AiResponseDTO response) { | ||
| 32 | + processedChunks++; | ||
| 33 | + | ||
| 34 | + // 更新总块数 | ||
| 35 | + if (response.getTotalChunks() != null && response.getTotalChunks() > 0) { | ||
| 36 | + this.totalChunks = response.getTotalChunks(); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + // 累积AI文本片段 | ||
| 40 | + if (StrUtil.isNotBlank(response.getTextFragment())) { | ||
| 41 | + aiTextBuilder.append(response.getTextFragment()); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + // 累积系统文本片段 | ||
| 45 | + if (StrUtil.isNotBlank(response.getSystemTextFragment())) { | ||
| 46 | + systemTextBuilder.append(response.getSystemTextFragment()); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + // 更新元数据(取最后一次非空值) | ||
| 50 | + if (StrUtil.isNotBlank(response.getSSceneName())) { | ||
| 51 | + this.sSceneName = response.getSSceneName(); | ||
| 52 | + } | ||
| 53 | + if (StrUtil.isNotBlank(response.getSMethodName())) { | ||
| 54 | + this.sMethodName = response.getSMethodName(); | ||
| 55 | + } | ||
| 56 | + if (StrUtil.isNotBlank(response.getSReturnType())) { | ||
| 57 | + this.sReturnType = response.getSReturnType(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + log.debug("累积进度: requestId={}, 已处理={}/{}块, AI文本长度={}", | ||
| 61 | + requestId, processedChunks, totalChunks, aiTextBuilder.length()); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 获取完整的响应 | ||
| 66 | + */ | ||
| 67 | + public AiResponseDTO getCompleteResponse() { | ||
| 68 | + AiResponseDTO response = new AiResponseDTO(); | ||
| 69 | + response.setRequestId(requestId); | ||
| 70 | + response.setAiText(aiTextBuilder.toString()); | ||
| 71 | + response.setSystemText(systemTextBuilder.toString()); | ||
| 72 | + response.setFullAiText(aiTextBuilder.toString()); | ||
| 73 | + response.setFullSystemText(systemTextBuilder.toString()); | ||
| 74 | + response.setSSceneName(sSceneName); | ||
| 75 | + response.setSMethodName(sMethodName); | ||
| 76 | + response.setSReturnType(sReturnType != null ? sReturnType : "MARKDOWN"); | ||
| 77 | + response.setTotalChunks(totalChunks); | ||
| 78 | + response.setChunkIndex(processedChunks - 1); | ||
| 79 | + response.setIsLastChunk(true); | ||
| 80 | + response.setElapsedTime(System.currentTimeMillis() - startTime); | ||
| 81 | + | ||
| 82 | + log.info("ERP累积完成: requestId={}, 总块数={}, AI文本长度={}, 系统文本长度={}, 耗时={}ms", | ||
| 83 | + requestId, totalChunks, | ||
| 84 | + aiTextBuilder.length(), systemTextBuilder.length(), | ||
| 85 | + response.getElapsedTime()); | ||
| 86 | + | ||
| 87 | + return response; | ||
| 88 | + } | ||
| 89 | +} | ||
| 0 | \ No newline at end of file | 90 | \ No newline at end of file |
src/main/java/com/xly/entity/AiResponseDTO.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/AiResponseDTO.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import com.xly.constant.ErrorCode; | ||
| 5 | +import com.xly.constant.ReturnTypeCode; | ||
| 6 | +import lombok.AllArgsConstructor; | ||
| 7 | +import lombok.Builder; | ||
| 8 | +import lombok.Data; | ||
| 9 | +import lombok.NoArgsConstructor; | ||
| 10 | + | ||
| 11 | +import java.io.Serializable; | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.Map; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * TTS响应数据传输对象 | ||
| 17 | + * 增强版:支持流式处理 | ||
| 18 | + */ | ||
| 19 | +@Data | ||
| 20 | +@Builder | ||
| 21 | +@NoArgsConstructor | ||
| 22 | +@AllArgsConstructor | ||
| 23 | +public class AiResponseDTO implements Serializable { | ||
| 24 | + | ||
| 25 | + private static final long serialVersionUID = 1L; | ||
| 26 | + | ||
| 27 | + // ============ 原有字段 ============ | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * AI文字部分 | ||
| 31 | + */ | ||
| 32 | + private String aiText; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 系统拼接返回的文字部分 | ||
| 36 | + */ | ||
| 37 | + private String systemText; | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 业务场景名称 | ||
| 41 | + */ | ||
| 42 | + private String sSceneName; | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 业务方法名称 | ||
| 46 | + */ | ||
| 47 | + private String sMethodName; | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 返回类型,默认MARKDOWN | ||
| 51 | + */ | ||
| 52 | + private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode(); | ||
| 53 | + | ||
| 54 | + // ============ 新增字段:流式处理支持 ============ | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * 请求ID,用于追踪整个流式请求 | ||
| 58 | + */ | ||
| 59 | + private String requestId; | ||
| 60 | + | ||
| 61 | + /** | ||
| 62 | + * 响应码 | ||
| 63 | + */ | ||
| 64 | + private Integer code; | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 响应消息 | ||
| 68 | + */ | ||
| 69 | + private String message; | ||
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * 处理状态:PROCESSING, COMPLETED, FAILED | ||
| 73 | + */ | ||
| 74 | + private String status; | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * 当前处理的块编号(从0开始) | ||
| 78 | + */ | ||
| 79 | + private Integer chunkIndex; | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * 总块数 | ||
| 83 | + */ | ||
| 84 | + private Integer totalChunks; | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 是否是最后一块 | ||
| 88 | + */ | ||
| 89 | + private Boolean isLastChunk; | ||
| 90 | + | ||
| 91 | + /** | ||
| 92 | + * 文本片段(用于流式传输) | ||
| 93 | + * 当aiText过大时,可以分段传输 | ||
| 94 | + */ | ||
| 95 | + private String textFragment; | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 系统文本片段(用于流式传输) | ||
| 99 | + */ | ||
| 100 | + private String systemTextFragment; | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 累积的完整AI文本(仅在最后一块时返回) | ||
| 104 | + */ | ||
| 105 | + private String fullAiText; | ||
| 106 | + | ||
| 107 | + /** | ||
| 108 | + * 累积的完整系统文本(仅在最后一块时返回) | ||
| 109 | + */ | ||
| 110 | + private String fullSystemText; | ||
| 111 | + | ||
| 112 | + /** | ||
| 113 | + * 处理进度(0-100) | ||
| 114 | + */ | ||
| 115 | + private Integer progress; | ||
| 116 | + | ||
| 117 | + /** | ||
| 118 | + * 时间戳 | ||
| 119 | + */ | ||
| 120 | + private Long timestamp; | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 处理耗时(毫秒) | ||
| 124 | + */ | ||
| 125 | + private Long elapsedTime; | ||
| 126 | + | ||
| 127 | + /** | ||
| 128 | + * 扩展字段,用于存储额外的元数据 | ||
| 129 | + */ | ||
| 130 | + private Map<String, Object> metadata; | ||
| 131 | + | ||
| 132 | + /** | ||
| 133 | + * 错误信息(当status=FAILED时) | ||
| 134 | + */ | ||
| 135 | + private String errorMessage; | ||
| 136 | + | ||
| 137 | + /** | ||
| 138 | + * 错误码(当status=FAILED时) | ||
| 139 | + */ | ||
| 140 | + private String errorCode; | ||
| 141 | + | ||
| 142 | + /** | ||
| 143 | + * 数据库类型 X: 向量库 S:数据库 | ||
| 144 | + */ | ||
| 145 | + private String dbType; | ||
| 146 | + | ||
| 147 | + /** | ||
| 148 | + * 数据库类型 H: 缓存 D: 动态 | ||
| 149 | + */ | ||
| 150 | + private String dbCach; | ||
| 151 | + | ||
| 152 | + /*** | ||
| 153 | + * @Author 钱豹 | ||
| 154 | + * @Date 15:15 2026/4/14 | ||
| 155 | + * @Param | ||
| 156 | + * @return | ||
| 157 | + * @Description 操作未清类型 | ||
| 158 | + **/ | ||
| 159 | + private String sCopyTo; | ||
| 160 | + /*** | ||
| 161 | + * @Author 钱豹 | ||
| 162 | + * @Date 15:16 2026/4/14 | ||
| 163 | + * @Param | ||
| 164 | + * @return | ||
| 165 | + * @Description 未清记录ID | ||
| 166 | + **/ | ||
| 167 | + private String sCopyToSrcId; | ||
| 168 | + | ||
| 169 | + // ============ 便捷方法 ============ | ||
| 170 | + | ||
| 171 | + /** | ||
| 172 | + * 判断是否处理成功 | ||
| 173 | + */ | ||
| 174 | + public boolean isSuccess() { | ||
| 175 | + return code != null && code == 200; | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /** | ||
| 179 | + * 判断是否处理中 | ||
| 180 | + */ | ||
| 181 | + public boolean isProcessing() { | ||
| 182 | + return "PROCESSING".equals(status); | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + /** | ||
| 186 | + * 判断是否已完成 | ||
| 187 | + */ | ||
| 188 | + public boolean isCompleted() { | ||
| 189 | + return "COMPLETED".equals(status); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + /** | ||
| 193 | + * 判断是否失败 | ||
| 194 | + */ | ||
| 195 | + public boolean isFailed() { | ||
| 196 | + return "FAILED".equals(status); | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + /** | ||
| 200 | + * 获取完整的文本(AI文本 + 系统文本) | ||
| 201 | + */ | ||
| 202 | + public String getFullText() { | ||
| 203 | + StringBuilder sb = new StringBuilder(); | ||
| 204 | + if (StrUtil.isNotBlank(aiText)) { | ||
| 205 | + sb.append(aiText); | ||
| 206 | + } | ||
| 207 | + if (StrUtil.isNotBlank(systemText)) { | ||
| 208 | + sb.append(systemText); | ||
| 209 | + } | ||
| 210 | + return sb.toString(); | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + /** | ||
| 214 | + * 创建处理中的响应 | ||
| 215 | + */ | ||
| 216 | + public static AiResponseDTO processing(String requestId, String textFragment, | ||
| 217 | + Integer chunkIndex, Integer totalChunks) { | ||
| 218 | + return AiResponseDTO.builder() | ||
| 219 | + .requestId(requestId) | ||
| 220 | + .code(200) | ||
| 221 | + .message("Processing") | ||
| 222 | + .status("PROCESSING") | ||
| 223 | + .textFragment(textFragment) | ||
| 224 | + .chunkIndex(chunkIndex) | ||
| 225 | + .totalChunks(totalChunks) | ||
| 226 | + .timestamp(System.currentTimeMillis()) | ||
| 227 | + .progress(calculateProgress(chunkIndex, totalChunks)) | ||
| 228 | + .build(); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + /** | ||
| 232 | + * 创建完成响应 | ||
| 233 | + */ | ||
| 234 | + public static AiResponseDTO completed(String requestId, String fullAiText, | ||
| 235 | + String fullSystemText, String sSceneName, | ||
| 236 | + String sMethodName, String sReturnType) { | ||
| 237 | + return AiResponseDTO.builder() | ||
| 238 | + .requestId(requestId) | ||
| 239 | + .code(200) | ||
| 240 | + .message("Completed") | ||
| 241 | + .status("COMPLETED") | ||
| 242 | + .fullAiText(fullAiText) | ||
| 243 | + .fullSystemText(fullSystemText) | ||
| 244 | + .aiText(fullAiText) | ||
| 245 | + .systemText(fullSystemText) | ||
| 246 | + .sSceneName(sSceneName) | ||
| 247 | + .sMethodName(sMethodName) | ||
| 248 | + .sReturnType(sReturnType) | ||
| 249 | + .progress(100) | ||
| 250 | + .timestamp(System.currentTimeMillis()) | ||
| 251 | + .isLastChunk(true) | ||
| 252 | + .build(); | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + /** | ||
| 256 | + * 创建失败响应 | ||
| 257 | + */ | ||
| 258 | + public static AiResponseDTO failed(String requestId, String errorMessage, String errorCode) { | ||
| 259 | + return AiResponseDTO.builder() | ||
| 260 | + .requestId(requestId) | ||
| 261 | + .code(500) | ||
| 262 | + .message("Failed") | ||
| 263 | + .status("FAILED") | ||
| 264 | + .errorMessage(errorMessage) | ||
| 265 | + .errorCode(errorCode) | ||
| 266 | + .timestamp(System.currentTimeMillis()) | ||
| 267 | + .build(); | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + /** | ||
| 271 | + * 计算进度 | ||
| 272 | + */ | ||
| 273 | + private static Integer calculateProgress(Integer chunkIndex, Integer totalChunks) { | ||
| 274 | + if (chunkIndex == null || totalChunks == null || totalChunks == 0) { | ||
| 275 | + return 0; | ||
| 276 | + } | ||
| 277 | + return (int) ((chunkIndex + 1) * 100.0 / totalChunks); | ||
| 278 | + } | ||
| 279 | +} | ||
| 0 | \ No newline at end of file | 280 | \ No newline at end of file |
src/main/java/com/xly/entity/AiSqlErrorHistory.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/AiSqlErrorHistory.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.util.Date; | ||
| 7 | + | ||
| 8 | +@Data | ||
| 9 | +public class AiSqlErrorHistory { | ||
| 10 | + | ||
| 11 | + private Integer iIncrement; | ||
| 12 | + private String sId; | ||
| 13 | + private Integer iOrder; | ||
| 14 | + private Date tCreateDate; | ||
| 15 | + private String sBrandsId; | ||
| 16 | + private String sSubsidiaryId; | ||
| 17 | + private String sMakePerson; | ||
| 18 | + private String sBillNo; | ||
| 19 | + private String sFormId= StrUtil.EMPTY; | ||
| 20 | + private Integer sStatus; | ||
| 21 | + private String sSceneId; | ||
| 22 | + private String sMethodId; | ||
| 23 | + private String sQuestion; | ||
| 24 | + private String sError_sql; | ||
| 25 | + private String sError_mes; | ||
| 26 | + private String sCorrect_sql; | ||
| 27 | + private Boolean bCheck = false; | ||
| 28 | + private Boolean bInvalid = false; | ||
| 29 | + private String sLoginId; | ||
| 30 | +} |
src/main/java/com/xly/entity/AiUserAgentQuestion.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/AiUserAgentQuestion.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.util.Date; | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | +@Data | ||
| 11 | +public class AiUserAgentQuestion { | ||
| 12 | + | ||
| 13 | + private Integer iIncrement; | ||
| 14 | + private String sId; | ||
| 15 | + private Integer iOrder; | ||
| 16 | + private Date tCreateDate; | ||
| 17 | + private String sBrandsId; | ||
| 18 | + private String sSubsidiaryId; | ||
| 19 | + private String sMakePerson; | ||
| 20 | + private String sBillNo; | ||
| 21 | + private String sFormId= StrUtil.EMPTY; | ||
| 22 | + private Integer sStatus; | ||
| 23 | + private String sSceneId; | ||
| 24 | + private String sMethodId; | ||
| 25 | + private String sQuestion; | ||
| 26 | + private String sSqlContent; | ||
| 27 | + private Boolean bCheck = false; | ||
| 28 | + private Boolean bInvalid = false; | ||
| 29 | + private String sLoginId; | ||
| 30 | + private String sQuestionGroupNo; | ||
| 31 | + private Boolean bRedis = false; | ||
| 32 | + private Date tUpdateDate; | ||
| 33 | + private Integer iUpdate; | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + | ||
| 37 | + | ||
| 38 | + | ||
| 39 | +} |
src/main/java/com/xly/entity/ConfirmationData.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/ConfirmationData.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.util.HashMap; | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 8 | +@Data | ||
| 9 | +public class ConfirmationData { | ||
| 10 | + private final String requestId; | ||
| 11 | + private final String toolName; | ||
| 12 | + private final Map<String, Object> parameters; | ||
| 13 | + private final String initialResult; | ||
| 14 | + private final long expiryTimestamp; // 超时时间戳 | ||
| 15 | + | ||
| 16 | + public ConfirmationData(String requestId, String toolName, | ||
| 17 | + Map<String, Object> parameters, | ||
| 18 | + String initialResult, | ||
| 19 | + long timeoutMillis) { | ||
| 20 | + this.requestId = requestId; | ||
| 21 | + this.toolName = toolName; | ||
| 22 | + this.parameters = parameters != null ? new HashMap<>(parameters) : new HashMap<>(); | ||
| 23 | + this.initialResult = initialResult; | ||
| 24 | + this.expiryTimestamp = System.currentTimeMillis() + timeoutMillis; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + // Getters 和 检查方法 | ||
| 28 | + public boolean isExpired() { | ||
| 29 | + return System.currentTimeMillis() > expiryTimestamp; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + public String getRequestId() { return requestId; } | ||
| 33 | + public String getToolName() { return toolName; } | ||
| 34 | + public Map<String, Object> getParameters() { return new HashMap<>(parameters); } | ||
| 35 | + public String getInitialResult() { return initialResult; } | ||
| 36 | + public long getExpiryTimestamp() { return expiryTimestamp; } | ||
| 37 | +} |
src/main/java/com/xly/entity/DynamicNl2SqlRequest.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/DynamicNl2SqlRequest.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import lombok.AllArgsConstructor; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import lombok.NoArgsConstructor; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 动态表结构的NL2SQL请求实体 | ||
| 10 | + * 适配多场景、任意表结构的NL2SQL查询 | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +@NoArgsConstructor | ||
| 14 | +@AllArgsConstructor | ||
| 15 | +public class DynamicNl2SqlRequest { | ||
| 16 | + /** 涉及表名(多表用,分隔,如product,sales、order,user) */ | ||
| 17 | + private String tableNames; | ||
| 18 | + /** 表结构详情(严格按格式:表名(字段1:类型,字段2:类型,主键/外键),多表换行/逗号分隔) */ | ||
| 19 | + private String tableStruct; | ||
| 20 | + /** 用户自然语言查询问题 */ | ||
| 21 | + private String question; | ||
| 22 | +} | ||
| 0 | \ No newline at end of file | 23 | \ No newline at end of file |
src/main/java/com/xly/entity/ErpDataset.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/ErpDataset.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 8 | +/*** | ||
| 9 | + * @Author 钱豹 | ||
| 10 | + * @Date 1:22 2026/2/6 | ||
| 11 | + * @Param | ||
| 12 | + * @return | ||
| 13 | + * @Description ERP 返回的结果集 | ||
| 14 | + **/ | ||
| 15 | +@Data | ||
| 16 | +public class ErpDataset { | ||
| 17 | + private Integer start; | ||
| 18 | + private Integer pageSize; | ||
| 19 | + private Integer totalCount; | ||
| 20 | + private Integer billNum; | ||
| 21 | + private Integer end; | ||
| 22 | + private Integer totalPageCount; | ||
| 23 | + private Integer previousPageNo; | ||
| 24 | + private Integer currentPageNo; | ||
| 25 | + private Integer nextPageNo; | ||
| 26 | + private List<ErpRow> rows; | ||
| 27 | +} |
src/main/java/com/xly/entity/ErpResult.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/ErpResult.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 22:42 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 参数实体类 | ||
| 11 | + **/ | ||
| 12 | +@Data | ||
| 13 | +public class ErpResult { | ||
| 14 | + | ||
| 15 | + private Integer code; | ||
| 16 | + private String msg; | ||
| 17 | + private ErpDataset dataset; | ||
| 18 | + | ||
| 19 | +} |
src/main/java/com/xly/entity/ErpRow.java
0 → 100644
src/main/java/com/xly/entity/Nl2SqlRequest.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/Nl2SqlRequest.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Data; | ||
| 5 | +import lombok.NoArgsConstructor; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * NL2SQL统一请求参数 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@NoArgsConstructor | ||
| 15 | +@AllArgsConstructor | ||
| 16 | +public class Nl2SqlRequest { | ||
| 17 | + /** 用户自然语言查询语句 */ | ||
| 18 | + private String question; | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + |
src/main/java/com/xly/entity/Nl2SqlResult.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/Nl2SqlResult.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Data; | ||
| 5 | +import lombok.NoArgsConstructor; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * NL2SQL统一返回结果 | ||
| 14 | + */ | ||
| 15 | +@Data | ||
| 16 | +@NoArgsConstructor | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +public class Nl2SqlResult { | ||
| 19 | + /** 生成并校验后的可执行MySQL SQL */ | ||
| 20 | + private String generateSql; | ||
| 21 | + /** SQL执行结构化结果 */ | ||
| 22 | + private List<Map<String, Object>> sqlResult; | ||
| 23 | + /** 结果自然语言解释 */ | ||
| 24 | + private String resultExplain; | ||
| 25 | +} | ||
| 26 | + |
src/main/java/com/xly/entity/ParamRule.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/ParamRule.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.time.LocalDateTime; | ||
| 6 | +import java.util.List; | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +/*** | ||
| 10 | + * @Author 钱豹 | ||
| 11 | + * @Date 22:42 2026/1/30 | ||
| 12 | + * @Param | ||
| 13 | + * @return | ||
| 14 | + * @Description 参数实体类 | ||
| 15 | + **/ | ||
| 16 | +@Data | ||
| 17 | +public class ParamRule { | ||
| 18 | + private Integer iOrder; | ||
| 19 | + private String sId; | ||
| 20 | + private String sParamValue; | ||
| 21 | + private String sParentId; | ||
| 22 | + private String sParam; | ||
| 23 | + private String sRule; | ||
| 24 | + private String sType; | ||
| 25 | + private String sExampleValue; | ||
| 26 | + private String sDefaultValue; | ||
| 27 | + private String sParamMissMemo; | ||
| 28 | + private String sParamConfig; | ||
| 29 | + private String sCopyTo; | ||
| 30 | + private String sRuleTs; | ||
| 31 | + private Boolean bTipModel; | ||
| 32 | + private Boolean bEmpty; | ||
| 33 | + private Boolean bConfirmAfter; | ||
| 34 | + private String sDynamicParamRuleSql; | ||
| 35 | + private String sDynamicParam; | ||
| 36 | + private String sDynamicParamName; | ||
| 37 | + private String sGroup; | ||
| 38 | + private Object sParamConfigValue; | ||
| 39 | + private Boolean bStandardParam; | ||
| 40 | + private Boolean bDynamicParam = false; | ||
| 41 | + | ||
| 42 | +} |
src/main/java/com/xly/entity/SceneDto.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/SceneDto.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.Column; | ||
| 4 | +import jakarta.persistence.Id; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.time.LocalDateTime; | ||
| 8 | + | ||
| 9 | +/*** | ||
| 10 | + * @Author 钱豹 | ||
| 11 | + * @Date 22:41 2026/1/30 | ||
| 12 | + * @Param | ||
| 13 | + * @return | ||
| 14 | + * @Description 模型实体 | ||
| 15 | + **/ | ||
| 16 | +@Data | ||
| 17 | +public class SceneDto { | ||
| 18 | + @Id | ||
| 19 | +// @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| 20 | + private String sId; | ||
| 21 | + | ||
| 22 | + @Column(name = "iOrder") | ||
| 23 | + private Integer iOrder; | ||
| 24 | + | ||
| 25 | + @Column(name = "tCreateDate") | ||
| 26 | + private LocalDateTime tCreateDate; | ||
| 27 | + | ||
| 28 | + @Column(name = "sMakePerson") | ||
| 29 | + private String sMakePerson; | ||
| 30 | + | ||
| 31 | + private String sBrandsId; | ||
| 32 | + | ||
| 33 | + private String sSubsidiaryId; | ||
| 34 | + | ||
| 35 | + private String sBillNo; | ||
| 36 | + | ||
| 37 | + private String sSceneNo; | ||
| 38 | + | ||
| 39 | + private String sSceneName; | ||
| 40 | + | ||
| 41 | + private String sNickName; | ||
| 42 | + | ||
| 43 | + private String sSceneContext; | ||
| 44 | + | ||
| 45 | + private String sStatus; | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + private LocalDateTime tUpdateDate; | ||
| 49 | + | ||
| 50 | +} |
src/main/java/com/xly/entity/SceneIntentParseReq.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/SceneIntentParseReq.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | +import lombok.Data; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 意图解析请求参数:传给大模型的入参,包含用户输入+可访问场景列表 | ||
| 6 | + */ | ||
| 7 | +@Data | ||
| 8 | +public class SceneIntentParseReq{ | ||
| 9 | + /** | ||
| 10 | + * 用户自然语言输入 | ||
| 11 | + */ | ||
| 12 | + private String userInput; | ||
| 13 | + /** | ||
| 14 | + * 该用户权限内的可访问场景(格式:场景编码-场景名称-支持功能,如ORDER_OPERATE-订单操作-下单、查订单、取消订单) | ||
| 15 | + */ | ||
| 16 | + private String authScenesDesc; | ||
| 17 | +} |
src/main/java/com/xly/entity/SceneIntentParseResp.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/SceneIntentParseResp.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Data; | ||
| 5 | +import lombok.NoArgsConstructor; | ||
| 6 | + | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 大模型意图解析响应DTO:强制大模型按此格式返回,方便后端解析 | ||
| 11 | + * 核心:仅返回场景编码(如ORDER_OPERATE),无需多余描述 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@NoArgsConstructor | ||
| 15 | +@AllArgsConstructor | ||
| 16 | +public class SceneIntentParseResp { | ||
| 17 | + /** | ||
| 18 | + * 匹配的业务场景编码,必须是BusinessScene的枚举名称(如ORDER_OPERATE/CUSTOMER_MANAGE/STOCK_QUERY) | ||
| 19 | + * 无匹配场景时,返回:NO_MATCH | ||
| 20 | + */ | ||
| 21 | + private String sceneCode; | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 业务场景名(如销量订单、送货单) | ||
| 26 | + */ | ||
| 27 | + private String scene; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 操作方法名(如增加、修改、审核、物流跟踪) | ||
| 31 | + */ | ||
| 32 | + private String method; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * JSON结构化参数(键=参数名,值=参数值,类型与工具描述一致) | ||
| 36 | + */ | ||
| 37 | + private Map<String, Object> params; | ||
| 38 | + | ||
| 39 | + | ||
| 40 | +} | ||
| 41 | + |
src/main/java/com/xly/entity/SceneTemplate.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/SceneTemplate.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.*; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 数据库存储的动态模板 | ||
| 8 | + */ | ||
| 9 | +@Entity | ||
| 10 | +@Table(name = "ai_scene_template") | ||
| 11 | +@Data | ||
| 12 | +public class SceneTemplate { | ||
| 13 | + | ||
| 14 | + @Id | ||
| 15 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| 16 | + private Long id; | ||
| 17 | + | ||
| 18 | + @Column(name = "scene_code") | ||
| 19 | + private String sceneCode; // 场景编码 | ||
| 20 | + | ||
| 21 | + @Column(name = "scene_name") | ||
| 22 | + private String sceneName; // 场景名称 | ||
| 23 | + | ||
| 24 | + @Column(name = "template_content", columnDefinition = "TEXT") | ||
| 25 | + private String templateContent; // 模板内容 | ||
| 26 | + | ||
| 27 | + @Column(name = "variables_config", columnDefinition = "JSON") | ||
| 28 | + private String variablesConfig; // 变量配置 | ||
| 29 | + | ||
| 30 | + @Column(name = "rules_config", columnDefinition = "JSON") | ||
| 31 | + private String rulesConfig; // 规则配置 | ||
| 32 | + | ||
| 33 | + @Column(name = "is_active") | ||
| 34 | + private Boolean active = true; | ||
| 35 | + | ||
| 36 | + public void setId(Long id) { | ||
| 37 | + this.id = id; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + public Long getId() { | ||
| 41 | + return id; | ||
| 42 | + } | ||
| 43 | +} | ||
| 0 | \ No newline at end of file | 44 | \ No newline at end of file |
src/main/java/com/xly/entity/ToolMeta.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/ToolMeta.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | +/*** | ||
| 11 | + * @Author 钱豹 | ||
| 12 | + * @Date 22:41 2026/1/30 | ||
| 13 | + * @Param | ||
| 14 | + * @return | ||
| 15 | + * @Description 工具实体 | ||
| 16 | + **/ | ||
| 17 | +@Data | ||
| 18 | +public class ToolMeta { | ||
| 19 | + private String sId; | ||
| 20 | + private Integer iOrder; | ||
| 21 | + private LocalDateTime tCreateDate; | ||
| 22 | + private String sBrandsId; | ||
| 23 | + private String sSubsidiaryId; | ||
| 24 | + private String sMakePerson; | ||
| 25 | + private String sBillNo; | ||
| 26 | + private String sFormId; | ||
| 27 | + private String sStatus; | ||
| 28 | + private String sceneName; | ||
| 29 | + private String sMethodName; | ||
| 30 | + private String stoolDesc; | ||
| 31 | + private String sParamRules; | ||
| 32 | + private Integer iBizType; | ||
| 33 | + private LocalDateTime tUpdateDate; | ||
| 34 | + private String sMethodNo; | ||
| 35 | + private String sControlName; | ||
| 36 | + private String sApiUrl; | ||
| 37 | + private String sSrcFormId; | ||
| 38 | + private String sBizContent; | ||
| 39 | + private String sSceneId; | ||
| 40 | + private String sInputTabelName; | ||
| 41 | + private String sStructureMemo; | ||
| 42 | + private String sAIshowfield; | ||
| 43 | + private List<Map<String,Object>> sAIshowfieldShow; | ||
| 44 | + private Integer iActionType; | ||
| 45 | + private String sendUrl; | ||
| 46 | + private List<ParamRule> paramRuleList;//模型 | ||
| 47 | + private List<ParamRule> paramRuleListCheck;//需要校验 | ||
| 48 | + private List<ParamRule> paramRuleListAll;//所有的 | ||
| 49 | + private List<ParamRule> dynamicParamRule;//动态参数 | ||
| 50 | + private LocalDateTime tMakeDate; | ||
| 51 | + private String sVectorfiled; | ||
| 52 | + private String sVectorjson; | ||
| 53 | + private String sVectorfiledAll; | ||
| 54 | + private String sVectorfiledShow; | ||
| 55 | + private String sTsMemo; | ||
| 56 | + private Boolean sQue = false; | ||
| 57 | + private Boolean bQuo = false; | ||
| 58 | + | ||
| 59 | +} |
src/main/java/com/xly/entity/UserSceneSession.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/entity/UserSceneSession.java | ||
| 1 | +package com.xly.entity; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import cn.hutool.core.util.ObjectUtil; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | +import java.util.concurrent.atomic.AtomicBoolean; | ||
| 10 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 11 | +import java.util.stream.Collectors; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * 用户会话状态:记录每个用户的场景选择状态,实现场景会话持久化 | ||
| 15 | + * 存储:用户ID、权限内场景、是否已选场景、当前选定场景 | ||
| 16 | + */ | ||
| 17 | +@Data | ||
| 18 | +public class UserSceneSession { | ||
| 19 | + /** | ||
| 20 | + * 唯一标识:用户ID | ||
| 21 | + */ | ||
| 22 | + private String userId; | ||
| 23 | + private String userName; | ||
| 24 | + private String authorization; | ||
| 25 | + | ||
| 26 | + private String sUserName; | ||
| 27 | + private String sBrandsId; | ||
| 28 | + private String sSubsidiaryId; | ||
| 29 | + /** | ||
| 30 | + * 该用户权限内可访问的所有场景(从权限映射中获取) | ||
| 31 | + */ | ||
| 32 | + private List<SceneDto> authScenes; | ||
| 33 | + /*** | ||
| 34 | + * 用户问题集合 | ||
| 35 | + **/ | ||
| 36 | + private List<String> sUserQuestionList; | ||
| 37 | + | ||
| 38 | + private List<ToolMeta> authTool; | ||
| 39 | + /** | ||
| 40 | + * 是否已选择场景(true=已选,进入专属场景交互;false=未选,先展示选择界面) | ||
| 41 | + */ | ||
| 42 | + private boolean sceneSelected = false; | ||
| 43 | + /** | ||
| 44 | + * 当前选定的业务场景(sceneSelected=true时才有值) | ||
| 45 | + */ | ||
| 46 | + private SceneDto currentScene; | ||
| 47 | + | ||
| 48 | + private ToolMeta currentTool; | ||
| 49 | + | ||
| 50 | + /*** | ||
| 51 | + * 当前未清返回的数据集 | ||
| 52 | + **/ | ||
| 53 | + private Map<Integer,Map<String,Object>> currentRowData; | ||
| 54 | + | ||
| 55 | + /*** | ||
| 56 | + * @Author 钱豹 | ||
| 57 | + * @Date 10:07 2026/1/31 | ||
| 58 | + * @Param | ||
| 59 | + * @return | ||
| 60 | + * @Description 当前已有参数 | ||
| 61 | + **/ | ||
| 62 | + private Map<String,Object> currentArgs; | ||
| 63 | + | ||
| 64 | + private String sFunPrompts; //方法返回的参数补全提示 | ||
| 65 | + | ||
| 66 | + private Boolean bCleanMemory = false; | ||
| 67 | + | ||
| 68 | + private Map<String, Object> args; | ||
| 69 | + /*** | ||
| 70 | + * @Author 钱豹 | ||
| 71 | + * @Date 12:40 2026/7/7 | ||
| 72 | + * @Param | ||
| 73 | + * @return | ||
| 74 | + * @Description 报价data | ||
| 75 | + **/ | ||
| 76 | + private Map<String, Object> quoData; | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 数据库类型 X: 向量库 S:数据库 | ||
| 80 | + */ | ||
| 81 | + private String dbType; | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * 数据库类型 H: 缓存 D: 动态 | ||
| 85 | + */ | ||
| 86 | + private String dbCach; | ||
| 87 | + /** | ||
| 88 | + * @Author 钱豹 | ||
| 89 | + * @Date 22:55 2026/4/13 | ||
| 90 | + * @Param | ||
| 91 | + * @return | ||
| 92 | + * @Description AI 模板 | ||
| 93 | + **/ | ||
| 94 | + private String sSystemPrompt; | ||
| 95 | + | ||
| 96 | + /*** | ||
| 97 | + * @Author 钱豹 | ||
| 98 | + * @Date 15:15 2026/4/14 | ||
| 99 | + * @Param | ||
| 100 | + * @return | ||
| 101 | + * @Description 操作未清类型 | ||
| 102 | + **/ | ||
| 103 | + private String sCopyTo; | ||
| 104 | + /*** | ||
| 105 | + * @Author 钱豹 | ||
| 106 | + * @Date 15:16 2026/4/14 | ||
| 107 | + * @Param | ||
| 108 | + * @return | ||
| 109 | + * @Description 未清记录ID | ||
| 110 | + **/ | ||
| 111 | + private String sCopyToSrcId; | ||
| 112 | + | ||
| 113 | + /*** | ||
| 114 | + * @Author 钱豹 | ||
| 115 | + * @Date 10:03 2026/4/24 | ||
| 116 | + * @Param | ||
| 117 | + * @return | ||
| 118 | + * @Description 工具是否已执行 | ||
| 119 | + **/ | ||
| 120 | + private Boolean toolExecuted=false; | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 构建场景选择提示语:展示权限内场景,引导用户选择 | ||
| 124 | + * @return 自然语言提示语 | ||
| 125 | + */ | ||
| 126 | + public String buildSceneSelectHint() { | ||
| 127 | + if (authScenes == null || authScenes.isEmpty()) { | ||
| 128 | + return "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>"; | ||
| 129 | + } | ||
| 130 | + // 按名称分组 | ||
| 131 | + StringBuilder sb = new StringBuilder("<div style='line-height:1.8;'>"); | ||
| 132 | + sb.append("<span>请输入序号或点链接进入</span>"); | ||
| 133 | + sb.append("<ul style='padding-left:10px;margin:0;list-style: none;'>"); | ||
| 134 | + for(int i=0;i<authScenes.size();i++){ | ||
| 135 | + SceneDto sceneDto = authScenes.get(i); | ||
| 136 | + sb.append("<li>").append(i+1).append("、").append("<span style=\"text-decoration: underline;\" data-action=\"reset\" data-text=\"") .append(i+1).append("\" onclick=\"reset('").append(i+1).append("')\">") .append(sceneDto.getSNickName()).append("</span><font style='color:#666; font-size:14px;'>(").append(sceneDto.getSSceneContext()).append(")</font>").append("</li>"); | ||
| 137 | + } | ||
| 138 | + sb.append("</ul>"); | ||
| 139 | + sb.append("<span style='color:red; font-size:14px;'>提示:可直接输入问题,系统会自动匹配智能体! </span>"); | ||
| 140 | + sb.append("</div>"); | ||
| 141 | + return sb.toString(); | ||
| 142 | + | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + | ||
| 146 | + // 新增:将权限内场景转换为「大模型可识别的描述字符串」 | ||
| 147 | + public String buildAuthScenesForLlm(List<ToolMeta> metasAll) { | ||
| 148 | + if (authScenes == null || authScenes.isEmpty()) { | ||
| 149 | + return "无可用场景"; | ||
| 150 | + } | ||
| 151 | + // 格式:场景编码-场景名称-支持功能,如ORDER_OPERATE-订单操作-下单、查订单、取消订单 | ||
| 152 | + return authScenes.stream() | ||
| 153 | + .map(scene -> "sceneCode:" + scene.getSSceneNo() + ",场景名称:" + scene.getSSceneName() + ",场景内容:\n" + getSceneContext(scene,metasAll)) | ||
| 154 | + .collect(Collectors.joining("\n")); | ||
| 155 | + } | ||
| 156 | + /*** | ||
| 157 | + * @Author 钱豹 | ||
| 158 | + * @Date 14:12 2026/2/6 | ||
| 159 | + * @Param [sceneDto, metasAll] | ||
| 160 | + * @return java.lang.String | ||
| 161 | + * @Description 方法名称拼接 | ||
| 162 | + **/ | ||
| 163 | + private String getSceneContext(SceneDto sceneDto,List<ToolMeta> metasAll){ | ||
| 164 | + List<ToolMeta> metas = metasAll.stream().filter(m->m.getSSceneId().equals(sceneDto.getSId())).collect(Collectors.toUnmodifiableList()); | ||
| 165 | + StringBuilder sb = new StringBuilder(); | ||
| 166 | + AtomicInteger index = new AtomicInteger(1); | ||
| 167 | + metas.forEach(m -> { | ||
| 168 | + sb.append(index.getAndIncrement()) | ||
| 169 | + .append(". 【") | ||
| 170 | +// .append(m.getSMethodNo()) | ||
| 171 | +// .append("-") | ||
| 172 | + .append(m.getSMethodName()) | ||
| 173 | + .append("】") | ||
| 174 | + .append(m.getStoolDesc()) | ||
| 175 | + .append("\n"); | ||
| 176 | + }); | ||
| 177 | + if(ObjectUtil.isEmpty(sb)){ | ||
| 178 | + sb.append(sceneDto.getSSceneContext()); | ||
| 179 | + } | ||
| 180 | + return sb.toString(); | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + /** | ||
| 184 | + * 根据用户输入的序号,匹配选定场景 | ||
| 185 | + * @param input 用户输入的序号(如1/2/3) | ||
| 186 | + * @return true=匹配成功,false=匹配失败 | ||
| 187 | + */ | ||
| 188 | + public boolean selectSceneByInput(String input) { | ||
| 189 | + try { | ||
| 190 | + int index = Integer.parseInt(input) - 1; | ||
| 191 | + if (index >= 0 && index < authScenes.size()) { | ||
| 192 | + this.currentScene = authScenes.get(index); | ||
| 193 | + this.sceneSelected = true; | ||
| 194 | + return true; | ||
| 195 | + } | ||
| 196 | + } catch (NumberFormatException e) { | ||
| 197 | + // 非数字输入,直接返回false | ||
| 198 | + } | ||
| 199 | + return false; | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | +} | ||
| 0 | \ No newline at end of file | 203 | \ No newline at end of file |
src/main/java/com/xly/exception/GlobalExceptionHandler.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/GlobalExceptionHandler.java | ||
| 1 | +package com.xly.exception; | ||
| 2 | + | ||
| 3 | +import com.xly.constant.ErrorCode; | ||
| 4 | +import com.xly.exception.dto.BusinessException; | ||
| 5 | +import com.xly.tts.bean.TTSResponseDTO; | ||
| 6 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 7 | +import jakarta.validation.ConstraintViolationException; | ||
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | +import org.junit.jupiter.api.Order; | ||
| 10 | +import org.springframework.core.Ordered; | ||
| 11 | +import org.springframework.dao.DataAccessException; | ||
| 12 | +import org.springframework.dao.DataIntegrityViolationException; | ||
| 13 | +import org.springframework.dao.DuplicateKeyException; | ||
| 14 | +import org.springframework.http.converter.HttpMessageConversionException; | ||
| 15 | +import org.springframework.jdbc.BadSqlGrammarException; | ||
| 16 | +import org.springframework.web.HttpMediaTypeNotSupportedException; | ||
| 17 | +import org.springframework.web.HttpRequestMethodNotSupportedException; | ||
| 18 | +import org.springframework.web.bind.MethodArgumentNotValidException; | ||
| 19 | +import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| 20 | +import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
| 21 | +import org.springframework.web.servlet.NoHandlerFoundException; | ||
| 22 | + | ||
| 23 | +import java.util.Date; | ||
| 24 | +import java.util.HashMap; | ||
| 25 | +import java.util.List; | ||
| 26 | +import java.util.Map; | ||
| 27 | +import java.util.stream.Collectors; | ||
| 28 | + | ||
| 29 | +@Slf4j | ||
| 30 | +@RestControllerAdvice | ||
| 31 | +@Order(Ordered.HIGHEST_PRECEDENCE) | ||
| 32 | +public class GlobalExceptionHandler { | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 处理所有异常 | ||
| 37 | + */ | ||
| 38 | + @ExceptionHandler(Exception.class) | ||
| 39 | + public TTSResponseDTO handleException(HttpServletRequest request, Exception e) { | ||
| 40 | + log.error("请求地址: {}, 全局异常: ", request.getRequestURI(), e); | ||
| 41 | + | ||
| 42 | + // 获取请求信息 | ||
| 43 | + String method = request.getMethod(); | ||
| 44 | + String uri = request.getRequestURI(); | ||
| 45 | + String queryString = request.getQueryString(); | ||
| 46 | + | ||
| 47 | + // 记录异常日志(可存入数据库) | ||
| 48 | +// ErrorLog errorLog = ErrorLog.builder() | ||
| 49 | +// .method(method) | ||
| 50 | +// .uri(uri) | ||
| 51 | +// .queryString(queryString) | ||
| 52 | +// .exceptionName(e.getClass().getName()) | ||
| 53 | +// .exceptionMessage(e.getMessage()) | ||
| 54 | +// .stackTrace(ExceptionUtils.getStackTrace(e)) | ||
| 55 | +// .ip(IpUtil.getIpAddr(request)) | ||
| 56 | +// .userAgent(request.getHeader("User-Agent")) | ||
| 57 | +// .timestamp(new Date()) | ||
| 58 | +// .build(); | ||
| 59 | +// | ||
| 60 | +// // 异步保存错误日志 | ||
| 61 | +// saveErrorLogAsync(errorLog); | ||
| 62 | + | ||
| 63 | + // 生产环境隐藏详细错误信息 | ||
| 64 | + if (isProduction()) { | ||
| 65 | + return TTSResponseDTO.error(ErrorCode.SYSTEM_ERROR); | ||
| 66 | + } else { | ||
| 67 | + // 开发环境返回详细错误信息 | ||
| 68 | + Map<String, Object> errorDetail = new HashMap<>(); | ||
| 69 | + errorDetail.put("exception", e.getClass().getName()); | ||
| 70 | + errorDetail.put("message", e.getMessage()); | ||
| 71 | + errorDetail.put("path", uri); | ||
| 72 | + errorDetail.put("method", method); | ||
| 73 | + errorDetail.put("timestamp", new Date()); | ||
| 74 | + return TTSResponseDTO.error(ErrorCode.SYSTEM_ERROR.getCode(), "系统异常: " + e.getMessage()); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 处理业务异常 | ||
| 80 | + */ | ||
| 81 | + @ExceptionHandler(BusinessException.class) | ||
| 82 | + public TTSResponseDTO handleBusinessException(BusinessException e) { | ||
| 83 | + log.warn("业务异常: {}", e.getMessage()); | ||
| 84 | + return TTSResponseDTO.error(e.getCode(), e.getMessage()); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + /** | ||
| 88 | + * 处理数据校验异常 | ||
| 89 | + */ | ||
| 90 | + @ExceptionHandler(MethodArgumentNotValidException.class) | ||
| 91 | + public TTSResponseDTO handleMethodArgumentNotValidException( | ||
| 92 | + MethodArgumentNotValidException e) { | ||
| 93 | + List<String> errors = e.getBindingResult().getFieldErrors() | ||
| 94 | + .stream() | ||
| 95 | + .map(error -> error.getField() + ": " + error.getDefaultMessage()) | ||
| 96 | + .collect(Collectors.toList()); | ||
| 97 | + | ||
| 98 | + String message = String.join("; ", errors); | ||
| 99 | + log.warn("参数校验失败: {}", message); | ||
| 100 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), message); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + /** | ||
| 104 | + * 处理约束违反异常 | ||
| 105 | + */ | ||
| 106 | + @ExceptionHandler(ConstraintViolationException.class) | ||
| 107 | + public TTSResponseDTO handleConstraintViolationException( | ||
| 108 | + ConstraintViolationException e) { | ||
| 109 | + List<String> errors = e.getConstraintViolations() | ||
| 110 | + .stream() | ||
| 111 | + .map(violation -> | ||
| 112 | + violation.getPropertyPath() + ": " + violation.getMessage()) | ||
| 113 | + .collect(Collectors.toList()); | ||
| 114 | + | ||
| 115 | + String message = String.join("; ", errors); | ||
| 116 | + log.warn("参数约束违反: {}", message); | ||
| 117 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), message); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 处理Http请求方法不支持异常 | ||
| 122 | + */ | ||
| 123 | + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | ||
| 124 | + public TTSResponseDTO handleHttpRequestMethodNotSupportedException( | ||
| 125 | + HttpRequestMethodNotSupportedException e) { | ||
| 126 | + log.warn("HTTP方法不支持: {} {}", e.getMethod(), e.getSupportedHttpMethods()); | ||
| 127 | + return TTSResponseDTO.error(ErrorCode.BAD_REQUEST.getCode(), | ||
| 128 | + "请求方法 '" + e.getMethod() + "' 不支持"); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * 处理媒体类型不支持异常 | ||
| 133 | + */ | ||
| 134 | + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) | ||
| 135 | + public TTSResponseDTO handleHttpMediaTypeNotSupportedException( | ||
| 136 | + HttpMediaTypeNotSupportedException e) { | ||
| 137 | + log.warn("媒体类型不支持: {}", e.getContentType()); | ||
| 138 | + return TTSResponseDTO.error(ErrorCode.BAD_REQUEST.getCode(), | ||
| 139 | + "媒体类型不支持: " + e.getContentType()); | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + /** | ||
| 143 | + * 处理404异常 | ||
| 144 | + */ | ||
| 145 | + @ExceptionHandler(NoHandlerFoundException.class) | ||
| 146 | + public TTSResponseDTO handleNoHandlerFoundException( | ||
| 147 | + NoHandlerFoundException e) { | ||
| 148 | + log.warn("接口不存在: {} {}", e.getHttpMethod(), e.getRequestURL()); | ||
| 149 | + return TTSResponseDTO.error(ErrorCode.NOT_FOUND.getCode(), | ||
| 150 | + "接口不存在: " + e.getRequestURL()); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + /** | ||
| 154 | + * 处理类型转换异常 | ||
| 155 | + */ | ||
| 156 | + @ExceptionHandler(HttpMessageConversionException.class) | ||
| 157 | + public TTSResponseDTO handleHttpMessageConversionException( | ||
| 158 | + HttpMessageConversionException e) { | ||
| 159 | + log.warn("HTTP消息转换异常: {}", e.getMessage()); | ||
| 160 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), | ||
| 161 | + "参数格式错误"); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + /** | ||
| 165 | + * 处理数据库异常 | ||
| 166 | + */ | ||
| 167 | + @ExceptionHandler(DataAccessException.class) | ||
| 168 | + public TTSResponseDTO handleDataAccessException(DataAccessException e) { | ||
| 169 | + log.error("数据库异常: {}", e.getMessage(), e); | ||
| 170 | + | ||
| 171 | + // 根据具体异常类型细化处理 | ||
| 172 | + if (e instanceof DuplicateKeyException) { | ||
| 173 | + return TTSResponseDTO.error(ErrorCode.DATA_EXISTS.getCode(), | ||
| 174 | + "数据已存在"); | ||
| 175 | + } else if (e instanceof DataIntegrityViolationException) { | ||
| 176 | + return TTSResponseDTO.error(ErrorCode.DATA_ERROR.getCode(), | ||
| 177 | + "数据完整性违反"); | ||
| 178 | + } else if (e instanceof BadSqlGrammarException) { | ||
| 179 | + return TTSResponseDTO.error(ErrorCode.DB_ERROR.getCode(), | ||
| 180 | + "SQL语法错误"); | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + return TTSResponseDTO.error(ErrorCode.DB_ERROR); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + /** | ||
| 187 | + * 处理JWT异常 | ||
| 188 | + */ | ||
| 189 | +// @ExceptionHandler(JwtException.class) | ||
| 190 | +// public TTSResponseDTO handleJwtException(JwtException e) { | ||
| 191 | +// log.warn("JWT异常: {}", e.getMessage()); | ||
| 192 | +// return ApiResult.error(ErrorCode.UNAUTHORIZED.getCode(), | ||
| 193 | +// "Token无效或已过期"); | ||
| 194 | +// } | ||
| 195 | + | ||
| 196 | +// /** | ||
| 197 | +// * 异步保存错误日志 | ||
| 198 | +// */ | ||
| 199 | +// @Async | ||
| 200 | +// public void saveErrorLogAsync(ErrorLog errorLog) { | ||
| 201 | +// try { | ||
| 202 | +// // 保存到数据库或日志文件 | ||
| 203 | +// errorLogService.save(errorLog); | ||
| 204 | +// } catch (Exception ex) { | ||
| 205 | +// log.error("保存错误日志失败", ex); | ||
| 206 | +// } | ||
| 207 | +// } | ||
| 208 | + | ||
| 209 | + /*** | ||
| 210 | + * @Author 钱豹 | ||
| 211 | + * @Date 23:21 2026/1/30 | ||
| 212 | + * @Param [] | ||
| 213 | + * @return boolean | ||
| 214 | + * @Description 判断是否生产环境还是开发环境 应该根据yml 配置来 后期拓展 | ||
| 215 | + **/ | ||
| 216 | + private boolean isProduction() { | ||
| 217 | + //TODO | ||
| 218 | + String profile = "dev"; | ||
| 219 | + return "prod".equals(profile); | ||
| 220 | + } | ||
| 221 | +} | ||
| 0 | \ No newline at end of file | 222 | \ No newline at end of file |
src/main/java/com/xly/exception/TtsExceptionFilter.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/TtsExceptionFilter.java | ||
| 1 | +//package com.xly.exception; | ||
| 2 | +// | ||
| 3 | +//import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | +//import com.xly.tts.bean.TTSResponseDTO; | ||
| 5 | +//import jakarta.servlet.*; | ||
| 6 | +//import jakarta.servlet.http.HttpServletRequest; | ||
| 7 | +//import jakarta.servlet.http.HttpServletResponse; | ||
| 8 | +//import lombok.RequiredArgsConstructor; | ||
| 9 | +//import lombok.extern.slf4j.Slf4j; | ||
| 10 | +//import org.springframework.core.annotation.Order; | ||
| 11 | +//import org.springframework.http.HttpStatus; | ||
| 12 | +//import org.springframework.http.MediaType; | ||
| 13 | +//import org.springframework.stereotype.Component; | ||
| 14 | +// | ||
| 15 | +//import java.io.IOException; | ||
| 16 | +// | ||
| 17 | +//@Slf4j | ||
| 18 | +//@Component | ||
| 19 | +//@Order(1) | ||
| 20 | +//@RequiredArgsConstructor | ||
| 21 | +//public class TtsExceptionFilter implements Filter { | ||
| 22 | +// | ||
| 23 | +// private final ObjectMapper objectMapper; | ||
| 24 | +// | ||
| 25 | +// @Override | ||
| 26 | +// public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) | ||
| 27 | +// throws IOException, ServletException { | ||
| 28 | +// | ||
| 29 | +// HttpServletRequest httpRequest = (HttpServletRequest) request; | ||
| 30 | +// HttpServletResponse httpResponse = (HttpServletResponse) response; | ||
| 31 | +// | ||
| 32 | +// // 只处理 TTS 相关接口 | ||
| 33 | +// if (!httpRequest.getRequestURI().contains("/api/tts/")) { | ||
| 34 | +// chain.doFilter(request, response); | ||
| 35 | +// return; | ||
| 36 | +// } | ||
| 37 | +// | ||
| 38 | +// try { | ||
| 39 | +// chain.doFilter(request, response); | ||
| 40 | +// } catch (Exception ex) { | ||
| 41 | +// log.error("TTS接口异常: {}", httpRequest.getRequestURI(), ex); | ||
| 42 | +// | ||
| 43 | +// // 清除之前的响应 | ||
| 44 | +// if (httpResponse.isCommitted()) { | ||
| 45 | +// return; | ||
| 46 | +// } | ||
| 47 | +// | ||
| 48 | +// httpResponse.reset(); | ||
| 49 | +// httpResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); | ||
| 50 | +// httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| 51 | +// httpResponse.setCharacterEncoding("UTF-8"); | ||
| 52 | +// | ||
| 53 | +// TTSResponseDTO errorResult = TTSResponseDTO.error( | ||
| 54 | +// HttpStatus.INTERNAL_SERVER_ERROR.value(), | ||
| 55 | +// "语音合成服务异常: " + ex.getMessage() | ||
| 56 | +// ); | ||
| 57 | +// | ||
| 58 | +// String jsonResponse = objectMapper.writeValueAsString(errorResult); | ||
| 59 | +// httpResponse.getWriter().write(jsonResponse); | ||
| 60 | +// httpResponse.getWriter().flush(); | ||
| 61 | +// } | ||
| 62 | +// } | ||
| 63 | +//} | ||
| 0 | \ No newline at end of file | 64 | \ No newline at end of file |
src/main/java/com/xly/exception/dto/AuthenticationException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/dto/AuthenticationException.java | ||
| 1 | +package com.xly.exception.dto; | ||
| 2 | + | ||
| 3 | +import com.xly.constant.ErrorCode; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:13 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 认证异常 | ||
| 11 | + **/ | ||
| 12 | +public class AuthenticationException extends BaseException { | ||
| 13 | + public AuthenticationException(ErrorCode errorCode) { | ||
| 14 | + super(errorCode); | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + public AuthenticationException(String message) { | ||
| 18 | + super(ErrorCode.UNAUTHORIZED.getCode(), message); | ||
| 19 | + } | ||
| 20 | +} | ||
| 0 | \ No newline at end of file | 21 | \ No newline at end of file |
src/main/java/com/xly/exception/dto/AuthorizationException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/dto/AuthorizationException.java | ||
| 1 | +package com.xly.exception.dto; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import com.xly.constant.ErrorCode; | ||
| 5 | + | ||
| 6 | +/*** | ||
| 7 | + * @Author 钱豹 | ||
| 8 | + * @Date 23:14 2026/1/30 | ||
| 9 | + * @Param | ||
| 10 | + * @return | ||
| 11 | + * @Description 授权异常 | ||
| 12 | + **/ | ||
| 13 | +public class AuthorizationException extends BaseException { | ||
| 14 | + public AuthorizationException(ErrorCode errorCode) { | ||
| 15 | + super(errorCode); | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + public AuthorizationException(String message) { | ||
| 19 | + super(ErrorCode.FORBIDDEN.getCode(), message); | ||
| 20 | + } | ||
| 21 | +} | ||
| 0 | \ No newline at end of file | 22 | \ No newline at end of file |
src/main/java/com/xly/exception/dto/BaseException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/dto/BaseException.java | ||
| 1 | +package com.xly.exception.dto; | ||
| 2 | + | ||
| 3 | +import com.xly.constant.ErrorCode; | ||
| 4 | +import lombok.Data; | ||
| 5 | +import lombok.EqualsAndHashCode; | ||
| 6 | + | ||
| 7 | +/*** | ||
| 8 | + * @Author 钱豹 | ||
| 9 | + * @Date 23:13 2026/1/30 | ||
| 10 | + * @Param | ||
| 11 | + * @return | ||
| 12 | + * @Description 基础异常定义 | ||
| 13 | + **/ | ||
| 14 | +@Data | ||
| 15 | +@EqualsAndHashCode(callSuper = true) | ||
| 16 | +public class BaseException extends RuntimeException { | ||
| 17 | + private final Integer code; | ||
| 18 | + private final String message; | ||
| 19 | + | ||
| 20 | + public BaseException(ErrorCode errorCode) { | ||
| 21 | + super(errorCode.getMessage()); | ||
| 22 | + this.code = errorCode.getCode(); | ||
| 23 | + this.message = errorCode.getMessage(); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + public BaseException(ErrorCode errorCode, String message) { | ||
| 27 | + super(message); | ||
| 28 | + this.code = errorCode.getCode(); | ||
| 29 | + this.message = message; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + public BaseException(Integer code, String message) { | ||
| 33 | + super(message); | ||
| 34 | + this.code = code; | ||
| 35 | + this.message = message; | ||
| 36 | + } | ||
| 37 | +} | ||
| 0 | \ No newline at end of file | 38 | \ No newline at end of file |
src/main/java/com/xly/exception/dto/BusinessException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/dto/BusinessException.java | ||
| 1 | +package com.xly.exception.dto; | ||
| 2 | + | ||
| 3 | +import com.xly.constant.ErrorCode; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:13 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 业务异常 | ||
| 11 | + **/ | ||
| 12 | +public class BusinessException extends BaseException { | ||
| 13 | + public BusinessException(ErrorCode errorCode) { | ||
| 14 | + super(errorCode); | ||
| 15 | + } | ||
| 16 | + public BusinessException(ErrorCode errorCode, String message) { | ||
| 17 | + super(errorCode, message); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + public BusinessException(Integer code, String message) { | ||
| 21 | + super(code, message); | ||
| 22 | + } | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + |
src/main/java/com/xly/exception/dto/DataException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/dto/DataException.java | ||
| 1 | +package com.xly.exception.dto; | ||
| 2 | + | ||
| 3 | +import com.xly.constant.ErrorCode; | ||
| 4 | + | ||
| 5 | +/*** | ||
| 6 | + * @Author 钱豹 | ||
| 7 | + * @Date 23:13 2026/1/30 | ||
| 8 | + * @Param | ||
| 9 | + * @return | ||
| 10 | + * @Description 数据异常 | ||
| 11 | + **/ | ||
| 12 | +public class DataException extends BaseException { | ||
| 13 | + public DataException(ErrorCode errorCode) { | ||
| 14 | + super(errorCode); | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + public DataException(String message) { | ||
| 18 | + super(ErrorCode.DATA_ERROR.getCode(), message); | ||
| 19 | + } | ||
| 20 | +} | ||
| 0 | \ No newline at end of file | 21 | \ No newline at end of file |
src/main/java/com/xly/exception/sqlexception/Nl2SqlBaseException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/sqlexception/Nl2SqlBaseException.java | ||
| 1 | +package com.xly.exception.sqlexception; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * NL2SQL根异常 | ||
| 5 | + */ | ||
| 6 | +public class Nl2SqlBaseException extends RuntimeException { | ||
| 7 | + public Nl2SqlBaseException(String message) { | ||
| 8 | + super(message); | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + public Nl2SqlBaseException(String message, Throwable cause) { | ||
| 12 | + super(message, cause); | ||
| 13 | + } | ||
| 14 | +} |
src/main/java/com/xly/exception/sqlexception/SqlExecuteException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/sqlexception/SqlExecuteException.java | ||
| 1 | +package com.xly.exception.sqlexception; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * SQL执行异常(数据库执行/结果解析失败) | ||
| 5 | + */ | ||
| 6 | +public class SqlExecuteException extends Nl2SqlBaseException { | ||
| 7 | + public SqlExecuteException(String message) { | ||
| 8 | + super(message); | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + public SqlExecuteException(String message, Throwable cause) { | ||
| 12 | + super(message, cause); | ||
| 13 | + } | ||
| 14 | +} | ||
| 0 | \ No newline at end of file | 15 | \ No newline at end of file |
src/main/java/com/xly/exception/sqlexception/SqlGenerateException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/sqlexception/SqlGenerateException.java | ||
| 1 | +package com.xly.exception.sqlexception; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * SQL生成异常(@AiService调用失败/生成空SQL) | ||
| 5 | + */ | ||
| 6 | +public class SqlGenerateException extends Nl2SqlBaseException { | ||
| 7 | + public SqlGenerateException(String message) { | ||
| 8 | + super(message); | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + public SqlGenerateException(String message, Throwable cause) { | ||
| 12 | + super(message, cause); | ||
| 13 | + } | ||
| 14 | +} | ||
| 0 | \ No newline at end of file | 15 | \ No newline at end of file |
src/main/java/com/xly/exception/sqlexception/SqlValidateException.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/exception/sqlexception/SqlValidateException.java | ||
| 1 | +package com.xly.exception.sqlexception; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * SQL强校验异常(语法错误/危险关键词/非SELECT语句) | ||
| 5 | + */ | ||
| 6 | +public class SqlValidateException extends Nl2SqlBaseException { | ||
| 7 | + public SqlValidateException(String message) { | ||
| 8 | + super(message); | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + public SqlValidateException(String message, Throwable cause) { | ||
| 12 | + super(message, cause); | ||
| 13 | + } | ||
| 14 | +} | ||
| 0 | \ No newline at end of file | 15 | \ No newline at end of file |
src/main/java/com/xly/mapper/AiToolDetailParamsMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/AiToolDetailParamsMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | +import com.xly.entity.ParamRule; | ||
| 4 | +import org.apache.ibatis.annotations.Mapper; | ||
| 5 | +import org.apache.ibatis.annotations.Select; | ||
| 6 | +import org.springframework.stereotype.Repository; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 10 | +@Repository | ||
| 11 | +@Mapper | ||
| 12 | +public interface AiToolDetailParamsMapper { | ||
| 13 | + | ||
| 14 | + // XML配置方式 | ||
| 15 | + @Select("SELECT * FROM ai_tool_detail_params ORDER BY iOrder,iIncrement ") | ||
| 16 | + List<ParamRule> findAll(); | ||
| 17 | +} | ||
| 0 | \ No newline at end of file | 18 | \ No newline at end of file |
src/main/java/com/xly/mapper/DynamicExeDbMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/DynamicExeDbMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import org.apache.ibatis.annotations.Mapper; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +@Repository | ||
| 12 | +@Mapper | ||
| 13 | +public interface DynamicExeDbMapper { | ||
| 14 | + | ||
| 15 | + /*** | ||
| 16 | + * @Author 钱豹 | ||
| 17 | + * @Date 22:43 2026/1/30 | ||
| 18 | + * @Param [] | ||
| 19 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | ||
| 20 | + * @Description 查询SQL执行 | ||
| 21 | + **/ | ||
| 22 | + List<Map<String,Object>> findSql(Map<String,Object> searMap); | ||
| 23 | + | ||
| 24 | + /*** | ||
| 25 | + * @Author 钱豹 | ||
| 26 | + * @Date 22:44 2026/1/30 | ||
| 27 | + * @Param [updMap] | ||
| 28 | + * @return void | ||
| 29 | + * @Description 更新SQL执行 | ||
| 30 | + **/ | ||
| 31 | + void updateSql(Map<String,Object> updMap); | ||
| 32 | + | ||
| 33 | + /*** | ||
| 34 | + * @Author 钱豹 | ||
| 35 | + * @Date 22:45 2026/1/30 | ||
| 36 | + * @Param [updMap] | ||
| 37 | + * @return void | ||
| 38 | + * @Description 新增SQL执行 | ||
| 39 | + **/ | ||
| 40 | + void addSql(Map<String,Object> addMap); | ||
| 41 | + | ||
| 42 | + /**** | ||
| 43 | + * @Author 钱豹 | ||
| 44 | + * @Date 22:56 2026/1/30 | ||
| 45 | + * @Param [delMap] | ||
| 46 | + * @return void | ||
| 47 | + * @Description 删除QL执行 | ||
| 48 | + **/ | ||
| 49 | + void delSql(Map<String,Object> delMap); | ||
| 50 | + | ||
| 51 | + | ||
| 52 | + /*** | ||
| 53 | + * @Author 钱豹 | ||
| 54 | + * @Date 22:45 2026/1/30 | ||
| 55 | + * @Param | ||
| 56 | + * @return | ||
| 57 | + * @Description 动态执行过程 并且有返回 执行过程 返回多个数据集 ,默认10个 | ||
| 58 | + **/ | ||
| 59 | + List<List<Map<String,Object>>> getCallProMoreResult(Map<String,Object> map); | ||
| 60 | + | ||
| 61 | + /*** | ||
| 62 | + * @Author 钱豹 | ||
| 63 | + * @Date 1:41 2026/2/4 | ||
| 64 | + * @Param [map] | ||
| 65 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | ||
| 66 | + * @Description 调用过程 | ||
| 67 | + **/ | ||
| 68 | + List<Map<String,Object>> getCallPro(Map<String,Object> map); | ||
| 69 | + | ||
| 70 | +} |
src/main/java/com/xly/mapper/ParamRuleMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/ParamRuleMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | +import com.xly.entity.ParamRule; | ||
| 4 | +import org.apache.ibatis.annotations.Mapper; | ||
| 5 | +import org.apache.ibatis.annotations.Select; | ||
| 6 | +import org.springframework.stereotype.Repository; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 10 | +@Repository | ||
| 11 | +@Mapper | ||
| 12 | +public interface ParamRuleMapper { | ||
| 13 | + | ||
| 14 | + // XML配置方式 | ||
| 15 | + @Select("SELECT * FROM ai_tool_detail_params") | ||
| 16 | + List<ParamRule> findAll(); | ||
| 17 | +} | ||
| 0 | \ No newline at end of file | 18 | \ No newline at end of file |
src/main/java/com/xly/mapper/SceneMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/SceneMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | +import com.xly.entity.SceneDto; | ||
| 4 | +import org.apache.ibatis.annotations.*; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +@Repository | ||
| 10 | +@Mapper | ||
| 11 | +public interface SceneMapper { | ||
| 12 | + | ||
| 13 | + // XML配置方式 | ||
| 14 | + @Select("SELECT * FROM ai_agent order by iOrder") | ||
| 15 | + List<SceneDto> findAll(); | ||
| 16 | +} | ||
| 0 | \ No newline at end of file | 17 | \ No newline at end of file |
src/main/java/com/xly/mapper/ToolMetaMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/ToolMetaMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import com.xly.entity.ToolMeta; | ||
| 5 | +import org.apache.ibatis.annotations.Mapper; | ||
| 6 | +import org.apache.ibatis.annotations.Select; | ||
| 7 | +import org.springframework.stereotype.Repository; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +@Repository | ||
| 12 | +@Mapper | ||
| 13 | +public interface ToolMetaMapper { | ||
| 14 | + | ||
| 15 | + // XML配置方式 | ||
| 16 | + @Select("SELECT A.* FROM ai_tool AS A order by iOrder") | ||
| 17 | + List<ToolMeta> findAll(); | ||
| 18 | +} | ||
| 0 | \ No newline at end of file | 19 | \ No newline at end of file |
src/main/java/com/xly/mapper/UserSceneSessionMapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/mapper/UserSceneSessionMapper.java | ||
| 1 | +package com.xly.mapper; | ||
| 2 | + | ||
| 3 | +import org.apache.ibatis.annotations.Mapper; | ||
| 4 | +import org.apache.ibatis.annotations.Param; | ||
| 5 | +import org.apache.ibatis.annotations.Select; | ||
| 6 | +import java.util.List; | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +@Mapper | ||
| 10 | +public interface UserSceneSessionMapper { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 查询用户权限Key列表 | ||
| 14 | + * @param sUserId 用户ID | ||
| 15 | + * @return 权限Key列表 | ||
| 16 | + */ | ||
| 17 | + @Select("SELECT J.sKey AS sKey FROM sysjurisdiction AS J JOIN sisjurisdictionclassify AS B ON J.sJurisdictionClassifyId = B.sId WHERE B.sId IN(SELECT U.sJurisdictionClassifyId FROM sftlogininfojurisdictiongroup AS U WHERE U.sParentId = #{sUserId}) ") | ||
| 18 | + List<Map<String, Object>> findUserPermissions(@Param("sUserId") String sUserId); | ||
| 19 | + | ||
| 20 | +} | ||
| 0 | \ No newline at end of file | 21 | \ No newline at end of file |
src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java | ||
| 1 | +package com.xly.milvus.bean; | ||
| 2 | + | ||
| 3 | +import io.milvus.grpc.SearchResultData; | ||
| 4 | +import io.milvus.response.SearchResultsWrapper; | ||
| 5 | +import java.util.List; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 自定义SearchResultsWrapper,用于访问protected方法 | ||
| 9 | + */ | ||
| 10 | +public class CustomSearchResultsWrapper extends SearchResultsWrapper { | ||
| 11 | + | ||
| 12 | + public CustomSearchResultsWrapper(SearchResultData results) { | ||
| 13 | + super(results); | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 公开访问getOutputFields方法 | ||
| 18 | + */ | ||
| 19 | + public List<String> getOutputFieldsPublic() { | ||
| 20 | + return super.getOutputFields(); | ||
| 21 | + } | ||
| 22 | +} | ||
| 0 | \ No newline at end of file | 23 | \ No newline at end of file |
src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java | ||
| 1 | +package com.xly.milvus.bean; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Builder; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import lombok.NoArgsConstructor; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | +import java.util.Map; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 相似度查询请求实体 | ||
| 13 | + */ | ||
| 14 | +@Data | ||
| 15 | +@Builder | ||
| 16 | +@NoArgsConstructor | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +public class SimilaritySearchRequest { | ||
| 19 | + private List<Float> queryVector; // 查询向量 | ||
| 20 | + private String queryText; // 查询文本(如果有文本转向量服务) | ||
| 21 | + private Integer topK = 10; // 返回数量 | ||
| 22 | + private Double minScore; // 最小相似度得分 | ||
| 23 | + private Double maxScore; // 最大相似度得分 | ||
| 24 | + private String metricType = "IP"; // 距离类型: IP(内积), L2(欧氏距离), COSINE(余弦) | ||
| 25 | + private List<String> outputFields; // 输出字段 | ||
| 26 | + private Map<String, Object> filter; // 过滤条件 | ||
| 27 | + private String partitionName; // 分区名称 | ||
| 28 | + private Boolean withScore = true; // 是否返回得分 | ||
| 29 | + private Boolean withDistance = false; // 是否返回距离 | ||
| 30 | +} | ||
| 0 | \ No newline at end of file | 31 | \ No newline at end of file |
src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java | ||
| 1 | +package com.xly.milvus.bean; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Builder; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import lombok.NoArgsConstructor; | ||
| 7 | + | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 相似度查询结果实体 - 增强版 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +@Builder | ||
| 15 | +@NoArgsConstructor | ||
| 16 | +@AllArgsConstructor | ||
| 17 | +public class SimilaritySearchResult { | ||
| 18 | + private Long id; // 整数ID | ||
| 19 | + private String strId; // 字符串ID | ||
| 20 | + private Float score; // 相似度得分 | ||
| 21 | + private Double normalizedScore; // 归一化后的得分(0-1之间) | ||
| 22 | + private Map<String, Object> fields; // 字段数据 | ||
| 23 | + private String collectionName; // 集合名称 | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 获取ID(优先返回字符串ID,如果没有则返回整数ID) | ||
| 27 | + */ | ||
| 28 | + public String getId() { | ||
| 29 | + if (strId != null && !strId.isEmpty()) { | ||
| 30 | + return strId; | ||
| 31 | + } | ||
| 32 | + return id != null ? String.valueOf(id) : null; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 获取字段值 | ||
| 37 | + */ | ||
| 38 | + public Object getField(String fieldName) { | ||
| 39 | + if (fields != null) { | ||
| 40 | + return fields.get(fieldName); | ||
| 41 | + } | ||
| 42 | + return null; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 获取字符串字段值 | ||
| 47 | + */ | ||
| 48 | + public String getStringField(String fieldName) { | ||
| 49 | + Object value = getField(fieldName); | ||
| 50 | + return value != null ? value.toString() : null; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 获取整数字段值 | ||
| 55 | + */ | ||
| 56 | + public Integer getIntField(String fieldName) { | ||
| 57 | + Object value = getField(fieldName); | ||
| 58 | + if (value instanceof Number) { | ||
| 59 | + return ((Number) value).intValue(); | ||
| 60 | + } | ||
| 61 | + return null; | ||
| 62 | + } | ||
| 63 | +} | ||
| 0 | \ No newline at end of file | 64 | \ No newline at end of file |
src/main/java/com/xly/milvus/config/EmbeddingConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/config/EmbeddingConfig.java | ||
| 1 | +package com.xly.milvus.config; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.model.embedding.EmbeddingModel; | ||
| 4 | +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; | ||
| 5 | +import org.springframework.context.annotation.Bean; | ||
| 6 | +import org.springframework.context.annotation.Configuration; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * LangChain4j 嵌入模型配置类 | ||
| 10 | + */ | ||
| 11 | +@Configuration | ||
| 12 | +public class EmbeddingConfig { | ||
| 13 | + | ||
| 14 | + @Bean | ||
| 15 | + public EmbeddingModel embeddingModel() { | ||
| 16 | + // 使用 All-MiniLM-L6-v2 嵌入模型 | ||
| 17 | + // 这是一个轻量级的模型,维度为384,适合在本地运行 | ||
| 18 | + return new AllMiniLmL6V2EmbeddingModel(); | ||
| 19 | + } | ||
| 20 | +} | ||
| 0 | \ No newline at end of file | 21 | \ No newline at end of file |
src/main/java/com/xly/milvus/config/MilvusConfig.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/config/MilvusConfig.java | ||
| 1 | +package com.xly.milvus.config; | ||
| 2 | + | ||
| 3 | +import io.milvus.v2.client.ConnectConfig; | ||
| 4 | +import io.milvus.v2.client.MilvusClientV2; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | +import org.springframework.beans.factory.annotation.Value; | ||
| 7 | +import org.springframework.context.annotation.Bean; | ||
| 8 | +import org.springframework.context.annotation.Configuration; | ||
| 9 | +import org.springframework.retry.annotation.EnableRetry; | ||
| 10 | +import org.springframework.retry.support.RetryTemplate; | ||
| 11 | + | ||
| 12 | +@Slf4j | ||
| 13 | +@Configuration | ||
| 14 | +@EnableRetry | ||
| 15 | +public class MilvusConfig { | ||
| 16 | + | ||
| 17 | + @Value("${milvus.host:localhost}") | ||
| 18 | + private String host; | ||
| 19 | + | ||
| 20 | + @Value("${milvus.port:19530}") | ||
| 21 | + private Integer port; | ||
| 22 | + | ||
| 23 | + @Value("${milvus.database:default}") | ||
| 24 | + private String database; | ||
| 25 | + | ||
| 26 | + @Value("${milvus.username:}") | ||
| 27 | + private String username; | ||
| 28 | + | ||
| 29 | + @Value("${milvus.password:}") | ||
| 30 | + private String password; | ||
| 31 | + | ||
| 32 | + @Value("${milvus.connect-timeout:10000}") | ||
| 33 | + private Long connectTimeout; | ||
| 34 | + | ||
| 35 | + @Value("${milvus.rpc-deadline:10000}") | ||
| 36 | + private Long rpcDeadline; | ||
| 37 | + | ||
| 38 | + @Value("${milvus.keep-alive-time:300000}") | ||
| 39 | + private Long keepAliveTime; | ||
| 40 | + | ||
| 41 | + @Value("${milvus.keep-alive-timeout:5000}") | ||
| 42 | + private Long keepAliveTimeout; | ||
| 43 | + | ||
| 44 | + @Value("${milvus.secure:false}") | ||
| 45 | + private Boolean secure; | ||
| 46 | + | ||
| 47 | + @Value("${milvus.validate-on-startup:true}") | ||
| 48 | + private boolean validateOnStartup; | ||
| 49 | + | ||
| 50 | + @Value("${milvus.fail-on-connect-error:false}") | ||
| 51 | + private boolean failOnConnectError; | ||
| 52 | + | ||
| 53 | + @Value("${milvus.enable-precheck:true}") | ||
| 54 | + private boolean enablePrecheck; | ||
| 55 | + | ||
| 56 | + @Bean(destroyMethod = "close") | ||
| 57 | + public MilvusClientV2 milvusClient() { | ||
| 58 | + try { | ||
| 59 | + String uri = String.format("http://%s:%d", host, port); | ||
| 60 | + // 构建连接配置 | ||
| 61 | + ConnectConfig.ConnectConfigBuilder configBuilder = ConnectConfig.builder() | ||
| 62 | + .uri(uri) | ||
| 63 | + .dbName("default") | ||
| 64 | + .connectTimeoutMs(connectTimeout) | ||
| 65 | + .rpcDeadlineMs(rpcDeadline) | ||
| 66 | + .keepAliveTimeMs(keepAliveTime) | ||
| 67 | + .keepAliveTimeoutMs(keepAliveTimeout) | ||
| 68 | + .keepAliveWithoutCalls(true) | ||
| 69 | + .secure(secure) | ||
| 70 | + .enablePrecheck(enablePrecheck); | ||
| 71 | + | ||
| 72 | + // 添加认证信息(如果有) | ||
| 73 | + if (username != null && !username.isEmpty()) { | ||
| 74 | + configBuilder.username(username); | ||
| 75 | + if (password != null) { | ||
| 76 | + configBuilder.password(password); | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + ConnectConfig connectConfig = configBuilder.build(); | ||
| 81 | + | ||
| 82 | + // 创建客户端 | ||
| 83 | + MilvusClientV2 client = new MilvusClientV2(connectConfig); | ||
| 84 | + // 检查xlyerp数据库是否存在,如果不存在则创建 | ||
| 85 | + var databases = client.listDatabases(); | ||
| 86 | + if (!databases.getDatabaseNames().contains(database)) { | ||
| 87 | + log.info("数据库 xlyerp 不存在,正在创建..."); | ||
| 88 | + client.createDatabase( | ||
| 89 | + io.milvus.v2.service.database.request.CreateDatabaseReq.builder() | ||
| 90 | + .databaseName(database) | ||
| 91 | + .build() | ||
| 92 | + ); | ||
| 93 | + log.info("数据库 xlyerp 创建成功"); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + // 切换到xlyerp数据库 | ||
| 97 | + client.useDatabase (database); | ||
| 98 | + log.info("切换到数据库: {}",database); | ||
| 99 | + | ||
| 100 | + // 启动验证 | ||
| 101 | + if (validateOnStartup) { | ||
| 102 | + validateConnection(client); | ||
| 103 | + } | ||
| 104 | + return client; | ||
| 105 | + } catch (Exception e) { | ||
| 106 | + log.error("创建Milvus客户端时发生异常: {}", e.getMessage(), e); | ||
| 107 | + throw new RuntimeException("无法创建Milvus客户端", e); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + /** | ||
| 113 | + * 验证Milvus连接 - 基于源码中可用的方法 | ||
| 114 | + */ | ||
| 115 | + private void validateConnection(MilvusClientV2 client) { | ||
| 116 | + try { | ||
| 117 | + // 方法1: 检查服务器版本(源码中存在getServerVersion()) | ||
| 118 | + String serverVersion = client.getServerVersion(); | ||
| 119 | + | ||
| 120 | + // 方法2: 列出数据库(源码中存在listDatabases()) | ||
| 121 | + var databases = client.listDatabases(); | ||
| 122 | + | ||
| 123 | + // 方法3: 检查健康状态(源码中存在checkHealth()) | ||
| 124 | + var health = client.checkHealth(); | ||
| 125 | + | ||
| 126 | + System.out.println("✅ Milvus连接成功"); | ||
| 127 | + System.out.println(" - 服务器版本: " + serverVersion); | ||
| 128 | + System.out.println(" - 数据库: " + databases); | ||
| 129 | + System.out.println(" - 健康状态: " + health); | ||
| 130 | + | ||
| 131 | + // 验证指定数据库是否存在 | ||
| 132 | + if (database != null && !database.isEmpty()) { | ||
| 133 | + boolean dbExists = databases.getDatabaseNames().contains(database); | ||
| 134 | + if (!dbExists && !"default".equals(database)) { | ||
| 135 | + System.err.println("⚠️ 警告: 指定数据库 '" + database + "' 不存在"); | ||
| 136 | + } | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + } catch (Exception e) { | ||
| 140 | + String errorMsg = String.format("❌ Milvus连接失败: %s:%d - %s",host, port, e.getMessage()); | ||
| 141 | + | ||
| 142 | + if (failOnConnectError) { | ||
| 143 | + throw new IllegalStateException(errorMsg, e); | ||
| 144 | + } else { | ||
| 145 | + System.err.println(errorMsg); | ||
| 146 | + System.err.println(" ⚠️ 应用将继续启动,但Milvus功能可能不可用"); | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + /** | ||
| 152 | + * 配置重试模板(可选) | ||
| 153 | + */ | ||
| 154 | + @Bean | ||
| 155 | + public RetryTemplate milvusRetryTemplate() { | ||
| 156 | + return RetryTemplate.builder() | ||
| 157 | + .maxAttempts(3) | ||
| 158 | + .exponentialBackoff(1000, 2, 10000) | ||
| 159 | + .retryOn(Exception.class) | ||
| 160 | + .build(); | ||
| 161 | + } | ||
| 162 | +} | ||
| 0 | \ No newline at end of file | 163 | \ No newline at end of file |
src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java
0 → 100644
| 1 | +++ a/src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java | ||
| 1 | +package com.xly.milvus.config; | ||
| 2 | + | ||
| 3 | +import io.milvus.v2.client.MilvusClientV2; | ||
| 4 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 5 | +import org.springframework.boot.actuate.health.Health; | ||
| 6 | +import org.springframework.boot.actuate.health.HealthIndicator; | ||
| 7 | +import org.springframework.stereotype.Component; | ||
| 8 | + | ||
| 9 | +import java.util.HashMap; | ||
| 10 | +import java.util.Map; | ||
| 11 | + | ||
| 12 | +@Component | ||
| 13 | +public class MilvusHealthIndicator implements HealthIndicator { | ||
| 14 | + | ||
| 15 | + @Autowired(required = false) | ||
| 16 | + private MilvusClientV2 milvusClient; | ||
| 17 | + | ||
| 18 | + @Override | ||
| 19 | + public Health health() { | ||
| 20 | + if (milvusClient == null) { | ||
| 21 | + return Health.down() | ||
| 22 | + .withDetail("error", "Milvus客户端未初始化") | ||
| 23 | + .build(); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + try { | ||
| 27 | + // 检查客户端是否就绪(源码中存在clientIsReady()) | ||
| 28 | + boolean isReady = milvusClient.clientIsReady(); | ||
| 29 | + | ||
| 30 | + if (!isReady) { | ||
| 31 | + return Health.down() | ||
| 32 | + .withDetail("error", "客户端未就绪") | ||
| 33 | + .build(); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + long startTime = System.currentTimeMillis(); | ||
| 37 | + | ||
| 38 | + // 执行健康检查 - 使用源码中存在的方法 | ||
| 39 | + String serverVersion = milvusClient.getServerVersion(); | ||
| 40 | + var databases = milvusClient.listDatabases(); | ||
| 41 | + var healthCheck = milvusClient.checkHealth(); | ||
| 42 | + | ||
| 43 | + long responseTime = System.currentTimeMillis() - startTime; | ||
| 44 | + | ||
| 45 | + Map<String, Object> details = new HashMap<>(); | ||
| 46 | + details.put("serverVersion", serverVersion); | ||
| 47 | + details.put("database", milvusClient.currentUsedDatabase()); | ||
| 48 | + details.put("databases", databases.getDatabaseNames()); | ||
| 49 | + details.put("healthStatus", healthCheck.getIsHealthy()); | ||
| 50 | + details.put("responseTime", responseTime + "ms"); | ||
| 51 | + details.put("clientReady", isReady); | ||
| 52 | + | ||
| 53 | + if (healthCheck.getQuotaStates() != null) { | ||
| 54 | + details.put("quotaStates", healthCheck.getQuotaStates()); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + return Health.up() | ||
| 58 | + .withDetails(details) | ||
| 59 | + .build(); | ||
| 60 | + | ||
| 61 | + } catch (Exception e) { | ||
| 62 | + return Health.down() | ||
| 63 | + .withDetail("error", e.getMessage()) | ||
| 64 | + .withDetail("clientReady", milvusClient.clientIsReady()) | ||
| 65 | + .build(); | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | +} | ||
| 0 | \ No newline at end of file | 69 | \ No newline at end of file |