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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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
| 1 | +++ a/src/main/java/com/xly/entity/ErpRow.java | |
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +import java.util.List; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +@Data | |
| 9 | +public class ErpRow { | |
| 10 | + private List<Map<String,Object>> dataSet; | |
| 11 | + private List<Map<String,Object>> sumSet; | |
| 12 | +} | ... | ... |
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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 69 | \ No newline at end of file | ... | ... |