Commit f3eace43b81d305ca3d1e9a373ab3367d614a5eb

Authored by qianbao
1 parent 3af94dd2

1111

Showing 99 changed files with 10891 additions and 0 deletions

Too many changes to show.

To preserve performance only 63 of 99 files are displayed.

.gitignore 0 → 100644
  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
  1 +wrapperVersion=3.3.4
  2 +distributionType=only-script
  3 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
... ...
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.debug-0.log 0 → 100644
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.error-0.log 0 → 100644
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.info-0.log 0 → 100644
mvnw 0 → 100644
  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 +<# : 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"
... ...
src/main/java/com/xly/XlyAiApplication.java 0 → 100644
  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/ChatiAgent.java 0 → 100644
  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 +public interface ChatiAgent {
  9 + @SystemMessage("""
  10 + 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。
  11 + 要求:1. 不生硬、不说教,避免书面化表达;
  12 + 2. 主动接梗,适当延伸话题,不一问一答;
  13 + 3. 偶尔带点小幽默,保持轻松无压力的氛围;
  14 + 4. 回答简洁,符合日常聊天的语气,不啰嗦。
  15 + 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。
  16 + """)
  17 + @UserMessage("用户说:{{userInput}}")
  18 + String chat(@MemoryId String userId, @V("userInput") String userInput);
  19 +}
... ...
src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java 0 → 100644
  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/5.7的标准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 + 4. 安全约束:禁止生成任何DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等),禁止使用子查询、存储过程、自定义函数、临时表;
  30 + 5. 精准性:严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件;
  31 + 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。
  32 + """)
  33 + @UserMessage("""
  34 + 【业务场景表结构信息】
  35 + 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN)
  36 + 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键))
  37 + 【用户需求】
  38 + {{userInput}}
  39 + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句:
  40 + """)
  41 + String generateMysqlSql(@MemoryId String userId,
  42 + @V("tableNames") String tableNames,
  43 + @V("tableStruct") String tableStruct,
  44 + @V("userInput") String userInput);
  45 +
  46 + /**
  47 + * 动态表结构:自然语言解释SQL执行结果
  48 + * 入参:用户问题、执行的SQL、表结构、JSON格式结果
  49 + */
  50 + @SystemMessage("""
  51 + 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
  52 + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
  53 + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
  54 + 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”;
  55 + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
  56 + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
  57 + """)
  58 + @UserMessage("""
  59 + 【业务场景表结构信息】
  60 + 表结构详情:{{tableStruct}}
  61 + 【查询相关信息】
  62 + 用户原始查询:{{userInput}}
  63 + 执行的MySQL SQL:{{sql}}
  64 + SQL执行结果(JSON格式):{{result}}
  65 + 请根据上述信息+通用规则,对查询结果做业务解释:
  66 + """)
  67 + String explainSqlResult(@MemoryId String userId,
  68 + @V("userInput") String userInput,
  69 + @V("sql") String sql,
  70 + @V("tableStruct") String tableStruct,
  71 + @V("result") String result);
  72 +}
0 73 \ No newline at end of file
... ...
src/main/java/com/xly/agent/ErpAiAgent.java 0 → 100644
  1 +package com.xly.agent;
  2 +
  3 +
  4 +import dev.langchain4j.service.MemoryId;
  5 +import dev.langchain4j.service.SystemMessage;
  6 +import dev.langchain4j.service.UserMessage;
  7 +import dev.langchain4j.service.V;
  8 +
  9 +/**
  10 + * 优化后:新增场景专属交互规则,大模型仅处理当前场景业务指令
  11 + */
  12 +public interface ErpAiAgent {
  13 + @SystemMessage("""
  14 + 你是一个专业的 工具方法匹配与参数提取 助手,核心职责是根据用户输入(含历史对话)精准匹配工具方法、提取参数、判断缺失并生成交互式补全提示;
  15 + 按严格按以下步骤处理,无任何额外输出!规则如下:
  16 + 1. 方法匹配:先精准拆解用户查询的核心业务意图,再自动匹配唯一符合用户问题的工具方法(MethodNo),禁止自创;
  17 + 2. 参数提取:提取该工具的全部参数,与描述完全一致,严格按标注类型赋值,数字无引号,为空时禁止赋值0;
  18 + """)
  19 + @UserMessage("用户输入:{{userInput}}")
  20 + String chat(@MemoryId String userId, @V("userInput") String userInput);
  21 +}
0 22 \ No newline at end of file
... ...
src/main/java/com/xly/agent/SceneSelectorAiAgent.java 0 → 100644
  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/productionmange),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/config/BizExecuteUtil.java 0 → 100644
  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 +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 +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 +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.memory.chat.MessageWindowChatMemory;
  9 +import dev.langchain4j.model.chat.ChatLanguageModel;
  10 +import dev.langchain4j.model.chat.StreamingChatLanguageModel;
  11 +import dev.langchain4j.model.ollama.OllamaChatModel;
  12 +import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
  13 +import dev.langchain4j.service.AiServices;
  14 +import org.springframework.beans.factory.annotation.Qualifier;
  15 +import org.springframework.beans.factory.annotation.Value;
  16 +import org.springframework.context.annotation.Bean;
  17 +import org.springframework.context.annotation.Configuration;
  18 +import org.springframework.context.annotation.Primary;
  19 +
  20 +import java.time.Duration;
  21 +
  22 +/**
  23 + * 大模型初始化配置(单例复用,避免重复创建)
  24 + */
  25 +@Configuration
  26 +public class ModelConfig {
  27 +
  28 +
  29 + @Value("${langchain4j.ollama.base-url}")
  30 + private String chatModelUrl;
  31 +
  32 + @Value("${langchain4j.ollama.base-url}")
  33 + private String sqlModelUrl;
  34 +
  35 + @Value("${langchain4j.ollama.chat-model-name}")
  36 + private String chatModelName;
  37 +
  38 + @Value("${langchain4j.ollama.sql-model-name}")
  39 + private String sqlModelName;
  40 +
  41 + // 中文对话模型 qwen2.5:7b-instruct
  42 + @Bean
  43 + @Primary
  44 + public OllamaChatModel chatLanguageModel() {
  45 + return OllamaChatModel.builder()
  46 + .baseUrl(chatModelUrl)
  47 + .modelName(chatModelName) // 使用聊天模型名称
  48 + .temperature(0.0) // 建议调整为0.0太确定
  49 + .topP(0.9)
  50 +// .numPredict(2048) // 添加生成长度限制
  51 + .timeout(Duration.ofSeconds(60)) // 缩短超时时间
  52 + .maxRetries(2)
  53 + .build();
  54 + }
  55 +
  56 + /***
  57 + * @Author 钱豹
  58 + * @Date 13:25 2026/2/6
  59 + * @Param []
  60 + * @return dev.langchain4j.model.ollama.OllamaChatModel
  61 + * @Description 聊天
  62 + **/
  63 + @Bean("chatiModel")
  64 + public ChatLanguageModel chatiModel() {
  65 + return OllamaChatModel.builder()
  66 + .baseUrl(chatModelUrl)
  67 + .modelName(chatModelName) // 使用聊天模型名称
  68 + .temperature(0.8) // 建议调整为0.8太确定
  69 + .topP(0.9)
  70 +// .numPredict(2048) // 添加生成长度限制
  71 + .timeout(Duration.ofSeconds(60)) // 缩短超时时间
  72 + .maxRetries(2)
  73 + .build();
  74 + }
  75 +
  76 + // SQL/代码专用模型 qwen2.5-coder:14b
  77 + @Bean("sqlChatModel") // 明确指定bean名称
  78 + public ChatLanguageModel sqlChatModel() {
  79 + return OllamaChatModel.builder()
  80 + .baseUrl(sqlModelUrl)
  81 + .modelName(sqlModelName) // 使用SQL模型名称
  82 + .temperature(0.0)
  83 + .topP(0.95)
  84 + .numPredict(4096) // 代码生成需要更长
  85 + .timeout(Duration.ofSeconds(120))
  86 + .maxRetries(3)
  87 +// .repeatPenalty(1.1) // 减少重复
  88 + .build();
  89 + }
  90 +
  91 + /***
  92 + * @Author 钱豹
  93 + * @Date 22:53 2026/2/3
  94 + * @Param []
  95 + * @return dev.langchain4j.model.chat.StreamingChatLanguageModel
  96 + * @Description 流式聊天模型 - 使用 @Primary
  97 + **/
  98 + @Bean("streamingChatModel")
  99 + @Primary
  100 + public StreamingChatLanguageModel streamingChatModel() {
  101 + return OllamaStreamingChatModel.builder()
  102 + .baseUrl(chatModelUrl)
  103 + .modelName(chatModelName)
  104 + .temperature(0.7)
  105 + .topP(0.9)
  106 + .numPredict(1024)
  107 + .timeout(Duration.ofSeconds(60))
  108 + .build();
  109 + }
  110 +
  111 + // 流式SQL/代码模型 - 指定名称
  112 + @Bean("streamingSqlModel")
  113 + public StreamingChatLanguageModel streamingSqlModel() {
  114 + return OllamaStreamingChatModel.builder()
  115 + .baseUrl(sqlModelUrl)
  116 + .modelName(sqlModelName)
  117 + .temperature(0.3)
  118 + .topP(0.95)
  119 + .numPredict(2048)
  120 + .timeout(Duration.ofSeconds(120))
  121 + .build();
  122 + }
  123 +
  124 + /**
  125 + * 全局ObjectMapper:支持LocalDate序列化
  126 + */
  127 + @Bean
  128 + @Primary
  129 + public ObjectMapper objectMapper() {
  130 + ObjectMapper mapper = new ObjectMapper();
  131 + mapper.registerModule(new JavaTimeModule());
  132 + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  133 + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  134 + return mapper;
  135 + }
  136 +
  137 + //动态SQL生成模型
  138 + @Bean
  139 + public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent(@Qualifier("sqlChatModel") ChatLanguageModel sqlModel) {
  140 + return AiServices.builder(DynamicTableNl2SqlAiAgent.class)
  141 + .chatLanguageModel(sqlModel)
  142 + // 会话记忆:每个用户最多保留10轮对话,避免记忆溢出
  143 + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
  144 + .build();
  145 + }
  146 +
  147 + //场景意图解析AI服务
  148 + @Bean
  149 + public SceneSelectorAiAgent sceneSelectorAiAgent(OllamaChatModel sqlModel) {
  150 + return AiServices.builder(SceneSelectorAiAgent.class)
  151 + .chatLanguageModel(sqlModel)
  152 + // 会话记忆:每个用户最多保留10轮对话,避免记忆溢出
  153 + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
  154 + .build();
  155 + }
  156 +}
0 157 \ No newline at end of file
... ...
src/main/java/com/xly/config/MvcConfig.java 0 → 100644
  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 +package com.xly.config;
  2 +
  3 +
  4 +import dev.langchain4j.memory.ChatMemory;
  5 +import dev.langchain4j.memory.chat.ChatMemoryProvider;
  6 +import dev.langchain4j.memory.chat.MessageWindowChatMemory;
  7 +import dev.langchain4j.data.message.ChatMessage;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +import java.util.Objects;
  13 +import java.util.concurrent.ConcurrentHashMap;
  14 +
  15 +/**
  16 + * 可操作的ChatMemoryProvider:获取消息对象+清除记忆(指定/全量)+删除单条消息
  17 + * 实现框架原生接口,无缝对接AiServices,线程安全适配生产环境
  18 + */
  19 +@Component
  20 +public class OperableChatMemoryProvider implements ChatMemoryProvider {
  21 + // 核心缓存:memoryId -> ChatMemory,保证一个会话/用户对应唯一记忆实例
  22 + private final Map<Object, ChatMemory> memoryCache = new ConcurrentHashMap<>();
  23 + // 记忆最大消息数,根据业务需求调整(原配置为10)
  24 + private static final int MAX_MESSAGE_SIZE = 100;
  25 +
  26 + /**
  27 + * 框架原生方法:获取【当前memoryId对应的ChatMemory实例(含消息对象)】
  28 + * AiServices自动调用,也是手动操作记忆/消息的唯一入口
  29 + */
  30 + @Override
  31 + public ChatMemory get(Object memoryId) {
  32 + // 空memoryId兜底,避免空指针
  33 + Object finalMemId = Objects.isNull(memoryId) ? "default_erp_chat_memory" : memoryId;
  34 + // 不存在则创建MessageWindowChatMemory,存在则复用
  35 + return memoryCache.computeIfAbsent(finalMemId, k -> MessageWindowChatMemory.withMaxMessages(MAX_MESSAGE_SIZE));
  36 + }
  37 +
  38 + // ===================== 1. 获取消息对象(当前memoryId) =====================
  39 + /**
  40 + * 获取当前会话/用户的全部消息列表(含用户消息、AI消息,按对话顺序)
  41 + */
  42 + public List<ChatMessage> getCurrentChatMessages(Object memoryId) {
  43 + if (Objects.isNull(memoryId)) {
  44 + return List.of();
  45 + }
  46 + // 从ChatMemory中获取原生消息列表
  47 + return this.get(memoryId).messages();
  48 + }
  49 +
  50 + // ===================== 2. 清除记忆(核心需求) =====================
  51 + /**
  52 + * 清空【指定memoryId】的全部记忆(最常用,如前端「清空对话」)
  53 + */
  54 + public void clearSpecifiedMemory(Object memoryId) {
  55 + if (Objects.nonNull(memoryId)) {
  56 + // 调用ChatMemory原生clear(),清空该实例所有消息
  57 + this.get(memoryId).clear();
  58 + }
  59 + }
  60 +
  61 + /**
  62 + * 全量清除【所有memoryId】的记忆(系统级清理,如定时任务/后台操作)
  63 + */
  64 + public void clearAllMemory() {
  65 + // 遍历所有记忆实例,逐个调用原生clear()清空
  66 + memoryCache.values().forEach(ChatMemory::clear);
  67 + // 可选:彻底清空缓存,销毁所有实例(释放内存,后续会重新创建)
  68 + // memoryCache.clear();
  69 + }
  70 +
  71 + // ===================== 3. 精准操作:删除单条消息 =====================
  72 + /**
  73 + * 删除当前memoryId的指定单条消息(如删除错误消息)
  74 + * @param message 要删除的消息对象(从getCurrentChatMessages中获取)
  75 + */
  76 + public void deleteSingleMessage(Object memoryId, ChatMessage message) {
  77 + if (Objects.nonNull(memoryId) && Objects.nonNull(message)) {
  78 + ChatMemory currentMemory = this.get(memoryId);
  79 + // 删除指定消息
  80 + currentMemory.messages().remove(message);
  81 + // 刷新记忆,保证MessageWindowChatMemory的最大消息数限制生效
  82 +// currentMemory.update(currentMemory.messages());
  83 + }
  84 + }
  85 +
  86 + /**
  87 + * 移除并清除指定记忆(清空消息+从缓存删除实例,彻底释放资源,适用于过期会话)
  88 + */
  89 + public void removeAndClearMemory(Object memoryId) {
  90 + if (Objects.nonNull(memoryId)) {
  91 + ChatMemory chatMemory = memoryCache.remove(memoryId);
  92 + if (Objects.nonNull(chatMemory)) {
  93 + chatMemory.clear();
  94 + }
  95 + }
  96 + }
  97 +}
... ...
src/main/java/com/xly/constant/BusinessCode.java 0 → 100644
  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 +
  18 + private final String code;
  19 + private final String message;
  20 +
  21 + BusinessCode(String code, String message) {
  22 + this.code = code;
  23 + this.message = message;
  24 + }
  25 +
  26 + /**
  27 + * 根据code获取ErrorCode
  28 + */
  29 + public static BusinessCode getByCode(Integer code) {
  30 + for (BusinessCode errorCode : values()) {
  31 + if (errorCode.getCode().equals(code)) {
  32 + return errorCode;
  33 + }
  34 + }
  35 + return QUOCONFIRM;
  36 + }
  37 +}
0 38 \ No newline at end of file
... ...
src/main/java/com/xly/constant/CommonConstant.java 0 → 100644
  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 +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 +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 +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 + MAKEDOWN("makedown", "makedown");
  18 +
  19 +
  20 + private final String code;
  21 + private final String message;
  22 +
  23 + ReturnTypeCode(String code, String message) {
  24 + this.code = code;
  25 + this.message = message;
  26 + }
  27 +
  28 + /**
  29 + * 根据code获取ErrorCode
  30 + */
  31 + public static ReturnTypeCode getByCode(String code) {
  32 + for (ReturnTypeCode errorCode : values()) {
  33 + if (errorCode.getCode().equals(code)) {
  34 + return errorCode;
  35 + }
  36 + }
  37 + return MAKEDOWN;
  38 + }
  39 +}
0 40 \ No newline at end of file
... ...
src/main/java/com/xly/constant/RuleCode.java 0 → 100644
  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 +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/AiResponseDTO.java 0 → 100644
  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 +
  13 +/**
  14 + * TTS响应数据传输对象
  15 + */
  16 +@Data
  17 +@Builder
  18 +@NoArgsConstructor
  19 +@AllArgsConstructor
  20 +public class AiResponseDTO implements Serializable {
  21 +
  22 + private static final long serialVersionUID = 1L;
  23 + // AI文字部分
  24 + private String aiText;
  25 + //系统拼接返回的文字部分
  26 + private String systemText;
  27 + //业务场景名称
  28 + private String sSceneName = StrUtil.EMPTY;
  29 + //业务方法名称
  30 + private String sMethodName = StrUtil.EMPTY;
  31 + private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode();
  32 +
  33 +}
0 34 \ No newline at end of file
... ...
src/main/java/com/xly/entity/ConfirmationData.java 0 → 100644
  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 +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 +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 +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 +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 +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 +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 +package com.xly.entity;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +/***
  8 + * @Author 钱豹
  9 + * @Date 22:42 2026/1/30
  10 + * @Param
  11 + * @return
  12 + * @Description 参数实体类
  13 + **/
  14 +@Data
  15 +public class ParamRule {
  16 + private Integer iOrder;
  17 + private String sParamValue;
  18 + private String sParentId;
  19 + private String sParam;
  20 + private String sRule;
  21 + private String sType;
  22 + private String sExampleValue;
  23 + private String sDefaultValue;
  24 + private String sParamMissMemo;
  25 + private String sParamConfig;
  26 + private String sCopyTo;
  27 + private String sRuleTs;
  28 + private Boolean bTipModel;
  29 + private Boolean bEmpty;
  30 + private Boolean bConfirmAfter;
  31 +
  32 +}
... ...
src/main/java/com/xly/entity/SceneDto.java 0 → 100644
  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 +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 +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 +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 +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 LocalDateTime tMakeDate;
  50 +}
... ...
src/main/java/com/xly/entity/UserSceneSession.java 0 → 100644
  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.AtomicInteger;
  10 +import java.util.stream.Collectors;
  11 +
  12 +/**
  13 + * 用户会话状态:记录每个用户的场景选择状态,实现场景会话持久化
  14 + * 存储:用户ID、权限内场景、是否已选场景、当前选定场景
  15 + */
  16 +@Data
  17 +public class UserSceneSession {
  18 + /**
  19 + * 唯一标识:用户ID
  20 + */
  21 + private String userId;
  22 + private String authorization;
  23 + /**
  24 + * 该用户权限内可访问的所有场景(从权限映射中获取)
  25 + */
  26 + private List<SceneDto> authScenes;
  27 +
  28 + private List<ToolMeta> authTool;
  29 + /**
  30 + * 是否已选择场景(true=已选,进入专属场景交互;false=未选,先展示选择界面)
  31 + */
  32 + private boolean sceneSelected = false;
  33 + /**
  34 + * 当前选定的业务场景(sceneSelected=true时才有值)
  35 + */
  36 + private SceneDto currentScene;
  37 +
  38 + private ToolMeta currentTool;
  39 + /***
  40 + * @Author 钱豹
  41 + * @Date 10:07 2026/1/31
  42 + * @Param
  43 + * @return
  44 + * @Description 当前已有参数
  45 + **/
  46 + private Map<String,Object> currentArgs;
  47 +
  48 + private String sFunPrompts; //方法返回的参数补全提示
  49 +
  50 + private Boolean bCleanMemory = false;
  51 + /**
  52 + * 构建场景选择提示语:展示权限内场景,引导用户选择
  53 + * @return 自然语言提示语
  54 + */
  55 + public String buildSceneSelectHint() {
  56 + if (authScenes == null || authScenes.isEmpty()) {
  57 + return "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>";
  58 + }
  59 + // 按名称分组
  60 + StringBuilder sb = new StringBuilder("<div style='line-height:1.8;'>");
  61 + sb.append("<span>请输入序号或点链接进入</span>");
  62 + sb.append("<ul style='padding-left:10px;margin:0;list-style: none;'>");
  63 + for(int i=0;i<authScenes.size();i++){
  64 + SceneDto sceneDto = authScenes.get(i);
  65 + 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>");
  66 + }
  67 + sb.append("</ul>");
  68 + sb.append("<span style='color:red; font-size:14px;'>提示:可直接输入问题,系统会自动匹配智能体! </span>");
  69 + sb.append("</div>");
  70 + return sb.toString();
  71 +
  72 + }
  73 +
  74 +
  75 + // 新增:将权限内场景转换为「大模型可识别的描述字符串」
  76 + public String buildAuthScenesForLlm(List<ToolMeta> metasAll) {
  77 + if (authScenes == null || authScenes.isEmpty()) {
  78 + return "无可用场景";
  79 + }
  80 + // 格式:场景编码-场景名称-支持功能,如ORDER_OPERATE-订单操作-下单、查订单、取消订单
  81 + return authScenes.stream()
  82 + .map(scene -> "sceneCode:" + scene.getSSceneNo() + ",场景名称:" + scene.getSSceneName() + ",场景内容:\n" + getSceneContext(scene,metasAll))
  83 + .collect(Collectors.joining("\n"));
  84 + }
  85 + /***
  86 + * @Author 钱豹
  87 + * @Date 14:12 2026/2/6
  88 + * @Param [sceneDto, metasAll]
  89 + * @return java.lang.String
  90 + * @Description 方法名称拼接
  91 + **/
  92 + private String getSceneContext(SceneDto sceneDto,List<ToolMeta> metasAll){
  93 + List<ToolMeta> metas = metasAll.stream().filter(m->m.getSSceneId().equals(sceneDto.getSId())).collect(Collectors.toUnmodifiableList());
  94 + StringBuilder sb = new StringBuilder();
  95 + AtomicInteger index = new AtomicInteger(1);
  96 + metas.forEach(m -> {
  97 + sb.append(index.getAndIncrement())
  98 + .append(". 【")
  99 +// .append(m.getSMethodNo())
  100 +// .append("-")
  101 + .append(m.getSMethodName())
  102 + .append("】")
  103 + .append(m.getStoolDesc())
  104 + .append("\n");
  105 + });
  106 + if(ObjectUtil.isEmpty(sb)){
  107 + sb.append(sceneDto.getSSceneContext());
  108 + }
  109 + return sb.toString();
  110 + }
  111 +
  112 + /**
  113 + * 根据用户输入的序号,匹配选定场景
  114 + * @param input 用户输入的序号(如1/2/3)
  115 + * @return true=匹配成功,false=匹配失败
  116 + */
  117 + public boolean selectSceneByInput(String input) {
  118 + try {
  119 + int index = Integer.parseInt(input) - 1;
  120 + if (index >= 0 && index < authScenes.size()) {
  121 + this.currentScene = authScenes.get(index);
  122 + this.sceneSelected = true;
  123 + return true;
  124 + }
  125 + } catch (NumberFormatException e) {
  126 + // 非数字输入,直接返回false
  127 + }
  128 + return false;
  129 + }
  130 +
  131 +
  132 +}
0 133 \ No newline at end of file
... ...
src/main/java/com/xly/exception/GlobalExceptionHandler.java 0 → 100644
  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 +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 +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 +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 +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 +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 +
  17 + public BusinessException(ErrorCode errorCode, String message) {
  18 + super(errorCode, message);
  19 + }
  20 +
  21 + public BusinessException(Integer code, String message) {
  22 + super(code, message);
  23 + }
  24 +}
  25 +
  26 +
  27 +
... ...
src/main/java/com/xly/exception/dto/DataException.java 0 → 100644
  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 +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 +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 +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 +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 +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")
  16 + List<ParamRule> findAll();
  17 +}
0 18 \ No newline at end of file
... ...
src/main/java/com/xly/mapper/DynamicExeDbMapper.java 0 → 100644
  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 +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 +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 +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 +
  19 +}
0 20 \ No newline at end of file
... ...
src/main/java/com/xly/mapper/UserSceneSessionMapper.java 0 → 100644
  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/runner/AppStartupRunner.java 0 → 100644
  1 +package com.xly.runner;
  2 +
  3 +import com.xly.entity.SceneDto;
  4 +import com.xly.entity.ToolMeta;
  5 +import com.xly.entity.ParamRule;
  6 +import com.xly.mapper.SceneMapper;
  7 +import com.xly.mapper.ParamRuleMapper;
  8 +import com.xly.mapper.ToolMetaMapper;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.boot.CommandLineRunner;
  12 +import org.springframework.core.annotation.Order;
  13 +import org.springframework.stereotype.Component;
  14 +
  15 +import java.util.ArrayList;
  16 +import java.util.Comparator;
  17 +import java.util.List;
  18 +import java.util.stream.Collectors;
  19 +
  20 +@Slf4j
  21 +@Component
  22 +@Order(100)
  23 +public class AppStartupRunner implements CommandLineRunner {
  24 +
  25 + // 静态缓存列表
  26 + private static final List<SceneDto> AI_AGENT_CACHE = new ArrayList<>();
  27 + private static final List<ToolMeta> AI_TOOL_CACHE = new ArrayList<>();
  28 + private static final List<ParamRule> AI_TOOL_PARAMS_CACHE = new ArrayList<>();
  29 +
  30 + // 使用实例变量进行依赖注入
  31 + @Autowired
  32 + private SceneMapper sceneMapper;
  33 +
  34 + @Autowired
  35 + private ParamRuleMapper paramRuleMapper;
  36 +
  37 + @Autowired
  38 + private ToolMetaMapper toolMetaMapper;
  39 +
  40 + public void cleanAllInit(){
  41 + AI_AGENT_CACHE.clear();
  42 + AI_TOOL_CACHE.clear();
  43 + AI_TOOL_PARAMS_CACHE.clear();
  44 + //初始化
  45 + initCache();
  46 + }
  47 +
  48 +
  49 +
  50 + @Override
  51 + public void run(String... args) throws Exception {
  52 + log.info("应用启动完成,开始初始化...");
  53 + // 初始化缓存
  54 + initCache();
  55 + // 打印缓存信息
  56 + printCacheInfo();
  57 + log.info("应用初始化完成");
  58 + }
  59 +
  60 + /**
  61 + * 配置热点数据加载
  62 + */
  63 + private void initCache() {
  64 + log.info("开始初始化缓存...");
  65 + // 1. 加载AI代理数据
  66 + List<SceneDto> sceneDtos = sceneMapper.findAll();
  67 + if (sceneDtos != null) {
  68 + AI_AGENT_CACHE.clear();
  69 + AI_AGENT_CACHE.addAll(sceneDtos);
  70 + log.info("加载AI代理数据: {} 条", sceneDtos.size());
  71 + } else {
  72 + log.warn("AI代理数据为空");
  73 + }
  74 +
  75 + // 2. 加载AI工具数据
  76 + List<ToolMeta> toolMetas = toolMetaMapper.findAll();
  77 + if (toolMetas != null) {
  78 + AI_TOOL_CACHE.clear();
  79 + AI_TOOL_CACHE.addAll(toolMetas);
  80 + log.info("加载AI工具数据: {} 条", toolMetas.size());
  81 + } else {
  82 + log.warn("AI工具数据为空");
  83 + }
  84 +
  85 + // 3. 加载AI工具参数数据
  86 + List<ParamRule> toolParams = paramRuleMapper.findAll();
  87 + if (toolParams != null) {
  88 + AI_TOOL_PARAMS_CACHE.clear();
  89 + AI_TOOL_PARAMS_CACHE.addAll(toolParams);
  90 + log.info("加载AI工具参数数据: {} 条", toolParams.size());
  91 + } else {
  92 + log.warn("AI工具参数数据为空");
  93 + }
  94 +
  95 + log.info("缓存初始化完成");
  96 + }
  97 +
  98 + /**
  99 + * 打印缓存信息
  100 + */
  101 + private void printCacheInfo() {
  102 + log.info("=== 缓存统计 ===");
  103 + log.info("AI代理缓存: {} 条", AI_AGENT_CACHE.size());
  104 + log.info("AI工具缓存: {} 条", AI_TOOL_CACHE.size());
  105 + log.info("AI工具参数缓存: {} 条", AI_TOOL_PARAMS_CACHE.size());
  106 + // 打印前几条数据示例
  107 + if (!AI_AGENT_CACHE.isEmpty()) {
  108 + log.info("AI代理示例: {}", AI_AGENT_CACHE.get(0));
  109 + }
  110 + if (!AI_TOOL_CACHE.isEmpty()) {
  111 + log.info("AI工具示例: {}", AI_TOOL_CACHE.get(0));
  112 + }
  113 + }
  114 +
  115 + /**
  116 + * 静态方法获取缓存(线程安全)
  117 + */
  118 + public static List<SceneDto> getAiAgentCache() {
  119 + return new ArrayList<>(AI_AGENT_CACHE); // 返回副本,保证线程安全
  120 + }
  121 +
  122 + public static List<ToolMeta> getAiToolCache() {
  123 + return new ArrayList<>(AI_TOOL_CACHE);
  124 + }
  125 +
  126 + public static List<ParamRule> getAiToolParamsCache() {
  127 + return new ArrayList<>(AI_TOOL_PARAMS_CACHE);
  128 + }
  129 +
  130 + public static List<ToolMeta> getTools(List<String> sModleData) {
  131 + List<ToolMeta> rData = AI_TOOL_CACHE.stream()
  132 + .filter(agent -> sModleData.contains(agent.getSSrcFormId()))
  133 + .collect(Collectors.toList());
  134 + rData.sort(Comparator.comparing(h -> h.getIOrder()));
  135 + return rData;
  136 + }
  137 +
  138 + public static List<ToolMeta> getAllTools() {
  139 + return AI_TOOL_CACHE;
  140 + }
  141 +
  142 + /**
  143 + * 根据ID获取AI代理
  144 + */
  145 + public static SceneDto getAiAgentById(String sId) {
  146 + return AI_AGENT_CACHE.stream()
  147 + .filter(agent -> agent.getSId().equals(sId))
  148 + .findFirst()
  149 + .orElse(null);
  150 + }
  151 + /**
  152 + * 根据编码获取AI代理
  153 + */
  154 + public static SceneDto getAiAgentByCode(String sCode) {
  155 + return AI_AGENT_CACHE.stream()
  156 + .filter(agent -> agent.getSSceneNo().equals(sCode))
  157 + .findFirst()
  158 + .orElse(null);
  159 + }
  160 +
  161 + /**
  162 + * 刷新缓存
  163 + */
  164 + public void refreshCache() {
  165 + initCache();
  166 + }
  167 +}
0 168 \ No newline at end of file
... ...
src/main/java/com/xly/service/DynamicExeDbService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +import cn.hutool.core.text.CharSequenceUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.json.JSONUtil;
  7 +import com.alibaba.fastjson.JSONObject;
  8 +import com.xly.constant.ErrorCode;
  9 +import com.xly.constant.ProcedureConstant;
  10 +import com.xly.exception.dto.BusinessException;
  11 +import com.xly.mapper.DynamicExeDbMapper;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Service;
  15 +
  16 +import java.util.*;
  17 +import java.util.stream.Stream;
  18 +
  19 +/***
  20 + * @Author 钱豹
  21 + * @Date 22:39 2026/1/30
  22 + * @Param
  23 + * @return
  24 + * @Description 动态执行SQL业务类
  25 + **/
  26 +@Slf4j
  27 +@Service("dynamicExeDbService")
  28 +@RequiredArgsConstructor
  29 +public class DynamicExeDbService {
  30 +
  31 + private final DynamicExeDbMapper dynamicExeDbMapper;
  32 + private static final String PARAMETER_NAME = "PARAMETER_NAME";
  33 + private static final String PARAMETER_MODE = "PARAMETER_MODE";
  34 + private static final String DATA_TYPE = "DATA_TYPE";
  35 + private static final String INT = "int";
  36 +
  37 +
  38 +
  39 + /***
  40 + * @Author 钱豹
  41 + * @Date 22:43 2026/1/30
  42 + * @Param []
  43 + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
  44 + * @Description 查询SQL执行
  45 + **/
  46 + public List<Map<String,Object>> findSql(Map<String,Object> searMap,String sSql){
  47 + Map<String,Object> findMap = new HashMap<>(searMap);
  48 + findMap.put("sSql",sSql);
  49 + return dynamicExeDbMapper.findSql(findMap);
  50 + }
  51 +
  52 + /***
  53 + * @Author 钱豹
  54 + * @Date 22:44 2026/1/30
  55 + * @Param [updMap]
  56 + * @return void
  57 + * @Description 更新SQL执行
  58 + **/
  59 + public void updateSql(Map<String,Object> updMap,String sSql){
  60 + Map<String,Object> updateMap = new HashMap<>(updMap);
  61 + updateMap.put("sSql",sSql);
  62 + dynamicExeDbMapper.updateSql(updateMap);
  63 + }
  64 +
  65 + /***
  66 + * @Author 钱豹
  67 + * @Date 22:45 2026/1/30
  68 + * @Param [updMap]
  69 + * @return void
  70 + * @Description 新增SQL执行
  71 + **/
  72 + public void addSql(Map<String,Object> addMap,String sSql){
  73 + Map<String,Object> addSql = new HashMap<>(addMap);
  74 + addSql.put("sSql",sSql);
  75 + dynamicExeDbMapper.addSql(addSql);
  76 + }
  77 +
  78 + /****
  79 + * @Author 钱豹
  80 + * @Date 22:56 2026/1/30
  81 + * @Param [delMap]
  82 + * @return void
  83 + * @Description 删除QL执行
  84 + **/
  85 + public void delSql(Map<String,Object> delMap,String sSql){
  86 + Map<String,Object> delSql = new HashMap<>(delMap);
  87 + delSql.put("sSql",sSql);
  88 + dynamicExeDbMapper.delSql(delSql);
  89 + }
  90 +
  91 +
  92 + /***
  93 + * @Author 钱豹
  94 + * @Date 22:57 2026/1/30
  95 + * @Param [searMap, sProName]
  96 + * @return java.util.List<java.util.List<java.util.Map<java.lang.String,java.lang.Object>>>
  97 + * @Description 动态SQL执行 默认10个结果集返回
  98 + **/
  99 + public List<List<Map<String,Object>>> getCallProMoreResult(Map<String, Object> searMap) {
  100 + List<List<Map<String,Object>>> outListData = null;
  101 + try{
  102 + //
  103 + outListData = this.dynamicExeDbMapper.getCallProMoreResult(searMap);
  104 + }catch (Exception e){
  105 + log.error("执行失败,失败原因:",e);
  106 + }
  107 + String sMassage = (ObjectUtil.isEmpty(searMap.get(ProcedureConstant.SRETURN)))? StrUtil.EMPTY:searMap.get(ProcedureConstant.SRETURN).toString();
  108 + String sCode = (ObjectUtil.isEmpty(searMap.get(ProcedureConstant.SCODE)))? StrUtil.EMPTY:searMap.get(ProcedureConstant.SCODE).toString();
  109 + if(ObjectUtil.isNotEmpty(sCode)
  110 + && Integer.valueOf(sCode)< 0){
  111 + throw new BusinessException(Integer.valueOf(sCode),sMassage);
  112 + }
  113 + return outListData;
  114 + }
  115 + /***
  116 + * @Author 钱豹
  117 + * @Date 9:33 2026/1/31
  118 + * @Param [searMap, sProName]
  119 + * @return java.util.Map<java.lang.String,java.lang.Object>
  120 + * @Description 调用过程
  121 + **/
  122 + public Map<String,Object> getCallPro(Map<String, Object> searMap, String sProName){
  123 + List<Map<String,Object>> outListData = null;
  124 + long oldRead = System.currentTimeMillis();
  125 + try{
  126 + outListData = this.dynamicExeDbMapper.getCallPro (searMap);
  127 + }catch (Exception e){
  128 + log.error("调用过程异常",e);
  129 + this.doException(e,searMap, sProName);
  130 + }
  131 + String sMsgText = null;
  132 + if(ObjectUtil.isNotEmpty(searMap.get(ProcedureConstant.SRETURN))){
  133 + sMsgText = searMap.get(ProcedureConstant.SRETURN).toString();
  134 + }
  135 + Map<String,Object> outMapData = new HashMap<>(8);
  136 + Set<String> outSet = new HashSet<>();
  137 + if(ObjectUtil.isNotEmpty(searMap.get(ProcedureConstant.OUTSETSTRING))){
  138 + outSet = (Set<String>) searMap.get(ProcedureConstant.OUTSETSTRING);
  139 + }
  140 + //返回出参返回的数据
  141 + outSet.forEach(out->{
  142 + try{
  143 + if(ObjectUtil.isNotEmpty(searMap.get(out))
  144 + && JSONUtil.isJson(searMap.get(out).toString())){
  145 + if(JSONUtil.isJsonObj(searMap.get(out).toString())){
  146 + outMapData.put(out, JSONObject.parseObject(searMap.get(out).toString()));
  147 + }else if(JSONUtil.isJsonArray(searMap.get(out).toString())){
  148 + outMapData.put(out, JSONObject.parseArray(searMap.get(out).toString()));
  149 + }
  150 + }else{
  151 + outMapData.put(out, searMap.get(out));
  152 + }
  153 + }catch (Exception e){
  154 + outMapData.put(out, searMap.get(out));
  155 + }
  156 + });
  157 + Map<String,Object> retMap = new HashMap<>(4);
  158 + retMap.put(ProcedureConstant.OUTLIST,ObjectUtil.isNotEmpty(outListData) ? outListData : new ArrayList<>());
  159 + retMap.put(ProcedureConstant.OUTMAP,ObjectUtil.isNotEmpty(outMapData) ? outMapData : new HashMap<>(4));
  160 + retMap.put(ProcedureConstant.SRETURN,ObjectUtil.isNotEmpty(sMsgText) ? sMsgText : "");
  161 + retMap.put(ProcedureConstant.SCODE,searMap.get(ProcedureConstant.SCODE));
  162 + return retMap;
  163 + }
  164 + /***
  165 + * @Author 钱豹
  166 + * @Date 9:22 2026/1/31
  167 + * @Param [searMap, sProName, msg]
  168 + * @return java.lang.String
  169 + * @Description 过程执行错误异常提醒
  170 + **/
  171 + public String getProExceptionMsg(Map<String, Object> searMap, String sProName , String msg){
  172 + StringBuffer sb = new StringBuffer();
  173 + try{
  174 + //根据过程名称获取过程参数
  175 + List<Map<String, Object>> proList = getProcParam(sProName);
  176 + sb.append("{ CALL ").append(sProName).append("(");
  177 + Stream.iterate(0, i -> i + 1).limit(proList.size()).forEach(i -> {
  178 + if(ProcedureConstant.OUT.equalsIgnoreCase(proList.get(i).get(PARAMETER_MODE).toString())){
  179 + if(i==0){
  180 + sb.append("@").append(proList.get(i).get(PARAMETER_NAME));
  181 + }else{
  182 + sb.append(",@").append(proList.get(i).get(PARAMETER_NAME));
  183 + }
  184 + }else{
  185 + if(ObjectUtil.isEmpty(searMap.get(proList.get(i).get(PARAMETER_NAME)))){
  186 + if(i==0){
  187 + sb.append("NULL");
  188 + }else{
  189 + sb.append(",NULL");
  190 + }
  191 + }else{
  192 + String sControl = ObjectUtil.isNotEmpty(searMap.get(proList.get(i).get(PARAMETER_NAME)))?
  193 + searMap.get(proList.get(i).get(PARAMETER_NAME)).toString(): CharSequenceUtil.EMPTY;
  194 + String sPlit = "'";
  195 + if(sControl.contains("'")){
  196 + sPlit = "\"";
  197 + }
  198 + if(i==0){
  199 + sb.append(sPlit).append(sControl).append(sPlit);
  200 + }else{
  201 + sb.append(",").append(sPlit).append(sControl).append(sPlit);
  202 + }
  203 + }
  204 + }
  205 + });
  206 + sb.append(") ");
  207 + msg = sb.toString();
  208 + }catch (Exception e){
  209 + e.printStackTrace();
  210 + }
  211 + return msg;
  212 + }
  213 +
  214 + public Map<String, Object> getDoProMap(String sProName, Map<String, Object> params) throws BusinessException{
  215 + Map<String,Object> searMap = new HashMap<>(4);
  216 + try{
  217 + //根据过程名称获取过程参数
  218 + //添加公司子公司
  219 +// if((ObjectUtil.isEmpty(params.get("sBrId"))
  220 +// || ObjectUtil.isEmpty(params.get("sSuId")))){
  221 +// params.put("sBrId",params.get("sBrandsId"));
  222 +// params.put("sSuId",params.get("sSubsidiaryId"));
  223 +// }
  224 +// if(ObjectUtil.isEmpty(params.get("sLoginId"))){
  225 +// params.put("sLoginId",params.get("sUserId"));
  226 +// }
  227 + List<Map<String, Object>> proList = getProcParam( sProName);
  228 + //{CALL mytest(#{ownerid,mode=IN,jdbcType=INTEGER},#{examcount,mode=OUT,jdbcType=INTEGER})}
  229 + StringBuffer sb = new StringBuffer();
  230 + sb.append("{CALL ").append(sProName).append("(");
  231 + Set<String> outSet = new HashSet<>(4);
  232 + Stream.iterate(0, i -> i + 1).limit(proList.size()).forEach(i -> {
  233 + if(i==0){
  234 + sb.append("#{").append(proList.get(i).get(PARAMETER_NAME)).append(",mode=").append(proList.get(i).get(PARAMETER_MODE));
  235 + }else{
  236 + sb.append(",#{").append(proList.get(i).get(PARAMETER_NAME)).append(",mode=").append(proList.get(i).get(PARAMETER_MODE));
  237 + }
  238 + if(ProcedureConstant.IN.equalsIgnoreCase(proList.get(i).get(PARAMETER_MODE).toString())){
  239 + if(params.get(proList.get(i).get(PARAMETER_NAME)) != null){
  240 + if(params.get(proList.get(i).get(PARAMETER_NAME)) instanceof List
  241 + || params.get(proList.get(i).get(PARAMETER_NAME)) instanceof Map){
  242 + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),
  243 + JSONUtil.toJsonStr(params.get(proList.get(i).get(PARAMETER_NAME)))
  244 + );
  245 + }else{
  246 + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),
  247 + params.get(proList.get(i).get(PARAMETER_NAME)));
  248 + }
  249 + }else{
  250 + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),null);
  251 + }
  252 +// if(proList.get(i).get(PARAMETER_NAME).toString().startsWith("s")){
  253 +// sb.append(",jdbcType=").append("LONGVARCHAR");
  254 +// }
  255 + }else{
  256 + if(proList.get(i).get(DATA_TYPE).equals(INT)){
  257 + sb.append(",jdbcType=").append("INTEGER");
  258 + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),0);
  259 + }else{
  260 + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),proList.get(i).get(PARAMETER_NAME).toString());
  261 + sb.append(",jdbcType=").append("LONGVARCHAR");
  262 + }
  263 + outSet.add(proList.get(i).get(PARAMETER_NAME).toString());
  264 + }
  265 + sb.append("}");
  266 + });
  267 + sb.append(")} ");
  268 + //获取过程返回
  269 + searMap.put("sSql",sb.toString());
  270 + searMap.put("outSet",outSet);
  271 + }catch (Exception e){
  272 + throw new BusinessException(ErrorCode.DB_ERROR,"解析获取过程查询sSql异常。异常原因:"+e.getMessage());
  273 + }
  274 + return searMap;
  275 + }
  276 +
  277 + /***
  278 + * @Author 钱豹
  279 + * @Date 9:25 2026/1/31
  280 + * @Param [proName]
  281 + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
  282 + * @Description 动态获取过程参数
  283 + **/
  284 + public List<Map<String, Object>> getProcParam(String proName){
  285 + StringBuffer paramSql = new StringBuffer();
  286 + Map<String, Object> serMap=new HashMap<>(4);
  287 + paramSql.append(" SELECT PARAMETER_NAME,PARAMETER_MODE,DATA_TYPE,ORDINAL_POSITION from information_schema.PARAMETERS ");
  288 + paramSql.append(" WHERE SPECIFIC_NAME=#{sProName}");
  289 + paramSql.append(" AND SPECIFIC_SCHEMA= DATABASE() ORDER BY ORDINAL_POSITION ASC");
  290 + serMap.put("sSql", paramSql.toString());
  291 + serMap.put("sProName",proName);
  292 + List<Map<String,Object>> paramList = dynamicExeDbMapper.findSql(serMap);
  293 + if(paramList==null || paramList.size()<1){
  294 + throw new BusinessException(ErrorCode.DB_ERROR,"数据库中未查询到过程,过程名称:"+proName+",请检查!");
  295 + }
  296 + return paramList;
  297 + }
  298 +
  299 + private void doException(Exception e,Map<String, Object> searMap,String sProName){
  300 + String msg = StrUtil.EMPTY;
  301 + String exMsg = "";
  302 + try{
  303 + msg = getProExceptionMsg(searMap, sProName,JSONUtil.toJsonStr(searMap));
  304 + if(ObjectUtil.isNotEmpty( e.getMessage())){
  305 + String[] msA= e.getMessage().split(":");
  306 + exMsg = msA[msA.length-1];
  307 + }
  308 + }catch (Exception ex){
  309 +
  310 + }
  311 + //-10标识过程中返回错误异常,非正常提示异常
  312 + throw new BusinessException(ErrorCode.DB_ERROR,"调用过程执行语句报错,SQL:"+msg+" ; 失败原因:"+ exMsg);
  313 + }
  314 +
  315 +}
0 316 \ No newline at end of file
... ...
src/main/java/com/xly/service/DynamicNl2SqlService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +
  4 +import com.alibaba.fastjson2.JSON;
  5 +
  6 +import com.xly.agent.DynamicTableNl2SqlAiAgent;
  7 +import com.xly.entity.DynamicNl2SqlRequest;
  8 +import com.xly.entity.Nl2SqlResult;
  9 +import com.xly.util.SqlExecuteUtil;
  10 +import com.xly.util.SqlValidateUtil;
  11 +import jakarta.annotation.Resource;
  12 +import org.slf4j.Logger;
  13 +import org.slf4j.LoggerFactory;
  14 +import org.springframework.stereotype.Service;
  15 +
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * 动态表结构的NL2SQL业务服务层
  21 + * 适配任意表结构、多业务场景
  22 + */
  23 +@Service
  24 +public class DynamicNl2SqlService {
  25 + private static final Logger log = LoggerFactory.getLogger(DynamicNl2SqlService.class);
  26 +
  27 + @Resource
  28 + private DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent;
  29 +
  30 + @Resource
  31 + private SqlExecuteUtil sqlExecuteUtil;
  32 +
  33 + /**
  34 + * 执行动态表结构的NL2SQL全流程
  35 + * @param request 动态表结构请求(含db名、表名、表结构、用户问题)
  36 + * @return 结构化NL2SQL结果
  37 + */
  38 + public Nl2SqlResult executeDynamicNl2Sql(DynamicNl2SqlRequest request) {
  39 + log.info("开始执行动态表结构NL2SQL流程,请求:{}", JSON.toJSONString(request));
  40 + // 1. 入参非空校验(重点校验表结构和用户问题)
  41 + if (request.getTableStruct() == null || request.getTableStruct().trim().isEmpty()
  42 + || request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
  43 + throw new IllegalArgumentException("表结构和用户查询问题不能为空");
  44 + }
  45 + // 补全默认值(如数据库名默认取配置中的值)
  46 +
  47 + String tableNames = request.getTableNames() == null ? "" : request.getTableNames().trim();
  48 + String rawSql = "select * from " + tableNames + " where 1=1 ";
  49 + // 2. 调用AI服务生成SQL(传入所有动态参数)
  50 +// String rawSql = dynamicTableNl2SqlAiAgent.generateMysqlSql(
  51 +// tableNames,
  52 +// request.getTableStruct().trim(),
  53 +// request.getQuestion().trim()
  54 +// );
  55 +// if (rawSql == null || rawSql.trim().isEmpty()) {
  56 +// throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空");
  57 +// }
  58 +// log.info("AI服务生成原始SQL:{}", rawSql);
  59 +
  60 + // 3. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
  61 + String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
  62 + SqlValidateUtil.validateMysqlSql(cleanSql);
  63 + log.info("SQL清理并强校验通过,可执行SQL:{}", cleanSql);
  64 +
  65 + // 4. 执行SQL获取结构化结果
  66 + List<Map<String, Object>> sqlResult = sqlExecuteUtil.executeSelectSql(cleanSql);
  67 + log.info("MySQL SQL执行完成,返回结果条数:{}", sqlResult.size());
  68 + String resultExplain ="";
  69 + // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务)
  70 + String resultJson = JSON.toJSONString(sqlResult);
  71 +// String resultExplain = dynamicTableNl2SqlAiAgent.explainSqlResult(
  72 +// request.getQuestion().trim(),
  73 +// cleanSql,
  74 +// request.getTableStruct().trim(),
  75 +// resultJson
  76 +// );
  77 +
  78 + // 6. 封装结果返回
  79 + Nl2SqlResult nl2SqlResult = new Nl2SqlResult();
  80 + nl2SqlResult.setGenerateSql(cleanSql);
  81 + nl2SqlResult.setSqlResult(sqlResult);
  82 + nl2SqlResult.setResultExplain(resultExplain);
  83 +
  84 + log.info("动态表结构NL2SQL流程执行完成");
  85 + return nl2SqlResult;
  86 + }
  87 +}
0 88 \ No newline at end of file
... ...
src/main/java/com/xly/service/ToolMetaService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +import com.xly.entity.ToolMeta;
  4 +import com.xly.mapper.ToolMetaMapper;
  5 +import lombok.RequiredArgsConstructor;
  6 +import org.springframework.stereotype.Service;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * 动态Tool Service实现
  12 + */
  13 +@Service("aiToolService")
  14 +@RequiredArgsConstructor
  15 +public class ToolMetaService {
  16 +
  17 + private final ToolMetaMapper toolMetaMapper;
  18 +
  19 + public List<ToolMeta> listAllEnable() {
  20 + return toolMetaMapper.findAll();
  21 + }
  22 +
  23 +}
0 24 \ No newline at end of file
... ...
src/main/java/com/xly/service/UserSceneSessionService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +import cn.hutool.core.util.ObjectUtil;
  4 +import com.xly.agent.ChatiAgent;
  5 +import com.xly.agent.DynamicTableNl2SqlAiAgent;
  6 +import com.xly.agent.ErpAiAgent;
  7 +import com.xly.config.OperableChatMemoryProvider;
  8 +import com.xly.entity.SceneDto;
  9 +import com.xly.entity.ToolMeta;
  10 +import com.xly.entity.UserSceneSession;
  11 +import com.xly.mapper.UserSceneSessionMapper;
  12 +import com.xly.runner.AppStartupRunner;
  13 +import dev.langchain4j.memory.chat.ChatMemoryProvider;
  14 +import org.springframework.beans.factory.annotation.Autowired;
  15 +import org.springframework.stereotype.Service;
  16 +
  17 +import java.util.*;
  18 +import java.util.stream.Collectors;
  19 +
  20 +@Service("userSceneSessionService")
  21 +public class UserSceneSessionService {
  22 +
  23 + // 新增核心缓存:用户场景会话状态(记录是否选场景、当前场景、权限内场景)
  24 + public static final Map<String, UserSceneSession> USER_SCENE_SESSION_CACHE = new HashMap<>();
  25 +
  26 + // 原有缓存:Agent实例、会话记忆
  27 + public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>();
  28 + public static final Map<String, ChatiAgent> CHAT_AGENT_CACHE = new HashMap<>();
  29 + public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>();
  30 +
  31 +
  32 +
  33 +
  34 +
  35 +
  36 +
  37 + @Autowired
  38 + private UserSceneSessionMapper modelMapper;
  39 +
  40 + public List<Map<String, Object>> getModelAllByUserId(String sUserId){
  41 + return modelMapper.findUserPermissions(sUserId);
  42 + }
  43 +
  44 + public List<ToolMeta> getModle(String sUserId, String sUserType){
  45 + List<Map<String, Object>> modleList = getModelAllByUserId(sUserId);
  46 + Set<String> dataSet = new HashSet<>();
  47 + if(ObjectUtil.isNotEmpty(modleList)){
  48 + modleList.forEach(one->{
  49 + String sKeys = one.get("sKey").toString();
  50 + String[] sIds = sKeys.split("-");
  51 + for(String sId:sIds){
  52 + if(ObjectUtil.isNotEmpty(sId)){
  53 + dataSet.add(sId);
  54 + }
  55 + }
  56 + });
  57 + }
  58 + List<String> data = new ArrayList<>(dataSet);
  59 + if("sysadmin".equals(sUserType)){
  60 + return AppStartupRunner.getAllTools();
  61 + }else {
  62 + return AppStartupRunner.getTools(data);
  63 + }
  64 + }
  65 + /***
  66 + * @Author 钱豹
  67 + * @Date 14:03 2026/2/10
  68 + * @Param []
  69 + * @return void
  70 + * @Description 清除所有记忆
  71 + **/
  72 + public void cleanAllSession(){
  73 + USER_SCENE_SESSION_CACHE.clear();
  74 + ERP_AGENT_CACHE.clear();
  75 + CHAT_AGENT_CACHE.clear();
  76 + ERP_DynamicTableNl2SqlAiAgent_CACHE.clear();
  77 + }
  78 + public UserSceneSession getUserSceneSession(String sUserId, String sUserType,String authorization){
  79 + if (USER_SCENE_SESSION_CACHE.containsKey(sUserId)) {
  80 + return USER_SCENE_SESSION_CACHE.get(sUserId);
  81 + }
  82 + UserSceneSession userSceneSession = new UserSceneSession();
  83 + List<ToolMeta> tools = getModle(sUserId,sUserType);
  84 + List<SceneDto> sceneDtos = getAiAgent(tools);
  85 + userSceneSession.setSceneSelected(false); // 初始状态:未选择场景
  86 + userSceneSession.setAuthorization(authorization);//存入用户token
  87 + userSceneSession.setUserId(sUserId);//存入用户ID
  88 + userSceneSession.setAuthTool(tools);//方法
  89 + userSceneSession.setAuthScenes(sceneDtos);//场景
  90 + // 3. 缓存会话
  91 + USER_SCENE_SESSION_CACHE.put(sUserId, userSceneSession);
  92 + return userSceneSession;
  93 + }
  94 +
  95 + public List<SceneDto> getAiAgent(List<ToolMeta> tools){
  96 + List<String> aiAgentIds = new ArrayList<>();
  97 + tools.forEach(tool->{
  98 + aiAgentIds.add(tool.getSSceneId());
  99 + });
  100 + List<SceneDto> agAll = AppStartupRunner.getAiAgentCache();
  101 + return agAll.stream().filter(one-> aiAgentIds.contains(one.getSId())).collect(Collectors.toList());
  102 + }
  103 +
  104 +}
... ...
src/main/java/com/xly/service/XlyErpService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +import cn.hutool.core.util.ObjectUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import com.alibaba.fastjson2.JSON;
  6 +import com.xly.agent.ChatiAgent;
  7 +import com.xly.agent.DynamicTableNl2SqlAiAgent;
  8 +import com.xly.agent.ErpAiAgent;
  9 +import com.xly.agent.SceneSelectorAiAgent;
  10 +import com.xly.config.OperableChatMemoryProvider;
  11 +import com.xly.constant.CommonConstant;
  12 +import com.xly.constant.ReturnTypeCode;
  13 +import com.xly.entity.*;
  14 +import com.xly.exception.sqlexception.SqlGenerateException;
  15 +import com.xly.mapper.ToolMetaMapper;
  16 +import com.xly.runner.AppStartupRunner;
  17 +import com.xly.tool.DynamicToolProvider;
  18 +import com.xly.util.InputPreprocessor;
  19 +import com.xly.util.SqlValidateUtil;
  20 +import com.xly.util.ValiDataUtil;
  21 +import dev.langchain4j.agent.tool.ToolExecutionRequest;
  22 +import dev.langchain4j.data.message.AiMessage;
  23 +import dev.langchain4j.model.chat.ChatLanguageModel;
  24 +import dev.langchain4j.model.ollama.OllamaChatModel;
  25 +import dev.langchain4j.service.AiServices;
  26 +import lombok.RequiredArgsConstructor;
  27 +import lombok.extern.slf4j.Slf4j;
  28 +import org.python.antlr.ast.Str;
  29 +import org.springframework.stereotype.Service;
  30 +
  31 +import java.math.BigDecimal;
  32 +import java.time.LocalDate;
  33 +import java.util.*;
  34 +
  35 +@Service
  36 +@RequiredArgsConstructor
  37 +@Slf4j
  38 +public class XlyErpService {
  39 + //中文对话模型
  40 + private final OllamaChatModel chatModel;
  41 + private final ChatLanguageModel chatiModel;
  42 + private final ChatLanguageModel sqlChatModel;
  43 + private final SceneSelectorAiAgent sceneSelectorAiAgent;
  44 + private final UserSceneSessionService userSceneSessionService;
  45 + private final DynamicToolProvider dynamicToolProvider;
  46 + private final OperableChatMemoryProvider operableChatMemoryProvider;
  47 + private final DynamicExeDbService dynamicExeDbService;
  48 + private final ToolMetaMapper toolMetaMapper;
  49 +
  50 +
  51 +
  52 + /***
  53 + * @Author 钱豹
  54 + * @Date 19:18 2026/1/27
  55 + * @Param [userInput, userId, sUserType]
  56 + * @return java.lang.String
  57 + * @Description 问答
  58 + **/
  59 + public AiResponseDTO erpUserInput(String userInput, String userId , String sUserType, String authorization) {
  60 + long startTime = System.currentTimeMillis();
  61 + try {
  62 + // 0. 预处理用户输入:去空格、转小写(方便匹配)
  63 + String input= InputPreprocessor.preprocessWithCommons(userInput);
  64 + // 1. 初始化用户场景会话(权限内场景)
  65 + UserSceneSession session = userSceneSessionService.getUserSceneSession(userId, sUserType,authorization);
  66 + session.setAuthorization(authorization);
  67 + session.setSFunPrompts(null);
  68 + // 2. 特殊指令:重置场景(无论是否已选,都可重置)
  69 + if (input.contains("重置") || input.contains("重新选择")) {
  70 + //清除记忆缓存
  71 + operableChatMemoryProvider.clearSpecifiedMemory(userId);
  72 + return AiResponseDTO.builder().aiText(resetUserScene(userId,session)).build();
  73 + }
  74 + //聊天只能体
  75 + if (session.getCurrentScene() != null
  76 + && Objects.equals(session.getCurrentScene().getSSceneNo(), "ChatZone"))
  77 + {
  78 + return getChatiAgent(input, session);
  79 + }
  80 +
  81 + // 3. 未选场景:先展示场景选择界面,处理用户序号选择
  82 + if (!session.isSceneSelected() && ValiDataUtil.me().isPureNumber(input)){
  83 + // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示)
  84 + return handleSceneSelect(userId, input, session);
  85 + }
  86 + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续
  87 + ErpAiAgent aiAgent = createErpAiAgent(userId, input, session);
  88 + // 没有选择到场景,进闲聊模式
  89 + if (aiAgent == null){
  90 + return getChatiAgent (input, session);
  91 + }
  92 + String sResponMessage = aiAgent.chat(userId, input);
  93 +// 调用方法,参数缺失部分提示,就直接使用方法返回的
  94 + if(session.getCurrentTool() != null
  95 + && session.getSFunPrompts()!=null
  96 + ){ // 缺失的参数明细
  97 + sResponMessage = session.getSFunPrompts();
  98 + }
  99 +// if (session.getCurrentTool()== null){
  100 +// sResponMessage = StrUtil.EMPTY;
  101 +// }
  102 + //5.执行工具方法后,清除记忆
  103 + if(session.getBCleanMemory()){
  104 + operableChatMemoryProvider.clearSpecifiedMemory(userId);
  105 + session.setBCleanMemory(false);
  106 + }
  107 +// 6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体
  108 + if((ObjectUtil.isNotEmpty(session.getCurrentTool())
  109 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
  110 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
  111 + ){
  112 + sResponMessage = getDynamicTableSql(session, input, userId, userInput);
  113 + }
  114 + //如果返回空的进入闲聊模式
  115 + if (ObjectUtil.isEmpty(sResponMessage)){
  116 + return getChatiAgent (input, session);
  117 + }
  118 + if (session.getCurrentScene()!= null ){
  119 + return AiResponseDTO.builder().aiText(sResponMessage)
  120 + .sSceneName(session.getCurrentScene().getSSceneName())
  121 + .sMethodName((ObjectUtil.isEmpty(session.getCurrentTool()))?StrUtil.EMPTY:session.getCurrentTool().getSMethodName())
  122 + .sReturnType(ReturnTypeCode.MAKEDOWN.getCode())
  123 + .build();
  124 + }else {
  125 + return AiResponseDTO.builder().aiText("当前场景:没有选择 退回当前场景 请输入 "+ CommonConstant.RESET + sResponMessage).sReturnType(ReturnTypeCode.HTML.getCode()).build();
  126 + }
  127 + } catch (Exception e) {
  128 + return AiResponseDTO.builder().aiText("系统异常:" + e.getMessage() + ",请稍后重试!").sReturnType(ReturnTypeCode.HTML.getCode()).build();
  129 + }
  130 + }
  131 +
  132 +
  133 + /***
  134 + * @Author 钱豹
  135 + * @Date 18:38 2026/2/5
  136 + * @Param [session, input, userId, userInput]
  137 + * @return java.lang.String
  138 + * @Description 获取执行动态SQL
  139 + **/
  140 + private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput){
  141 + String resultExplain = StrUtil.EMPTY;
  142 + try{
  143 + // 1. 构建自然语言转SQLAgent,
  144 + DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session);
  145 + String tableNames = session.getCurrentTool().getSInputTabelName();
  146 + // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart";
  147 + String tableStruct = session.getCurrentTool().getSStructureMemo();
  148 + String rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,userInput);
  149 + if (rawSql == null || rawSql.trim().isEmpty()) {
  150 + throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空");
  151 + }
  152 + // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
  153 + String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
  154 + SqlValidateUtil.validateMysqlSql(cleanSql);
  155 + log.info("SQL清理并强校验通过,可执行SQL:{}", cleanSql);
  156 + // 4. 执行SQL获取结构化结果
  157 +// Map<String,Object> params = new HashMap<>();
  158 + List<Map<String, Object>> sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql);
  159 + // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务)
  160 + String resultJson = JSON.toJSONString(sqlResult);
  161 + resultExplain = aiDynamicTableNl2SqlAiAgent.explainSqlResult(
  162 + userId,
  163 + userInput,
  164 + cleanSql,
  165 + tableStruct,
  166 + resultJson
  167 + );
  168 + }catch (Exception e){
  169 + session.setCurrentTool(null);
  170 + resultExplain = "动态SQL执行错误,请提供更具体的问题或指令";
  171 + }
  172 + log.info("动态表结构NL2SQL流程执行完成");
  173 + return resultExplain;
  174 + }
  175 +
  176 +
  177 +
  178 +
  179 + /***
  180 + * @Author 钱豹
  181 + * @Date 11:22 2026/1/31
  182 + * @Param
  183 + * @return
  184 + * @Description 动态参数补齐处理
  185 + **/
  186 + private String dotoolExecutionRequests(AiMessage aiMessage){
  187 + String textTs = aiMessage.text();
  188 + if(aiMessage.hasToolExecutionRequests()){
  189 + List<ToolExecutionRequest> toolExecutionRequests = aiMessage.toolExecutionRequests();
  190 + toolExecutionRequests.forEach(toolRequests->{
  191 + String arguments = toolRequests.arguments();
  192 + log.info(arguments);
  193 + });
  194 + }
  195 + return textTs;
  196 + }
  197 +
  198 + /***
  199 + * 存入全部场景
  200 + * @Author 钱豹
  201 + * @Date 19:06 2026/1/26
  202 + * @Param [sUserId, sUserType]
  203 + * @return java.lang.String
  204 + * 页面刷新/首次进入时调用:初始化用户场景会话,直接返回场景选择引导词
  205 + * 前端页面加载完成后,无需用户输入,直接调用该方法即可显示引导词
  206 + * @param sUserId 用户ID(前端传入,如user-001) sUserType 角色状态
  207 + * @return 场景选择引导词(即原buildSceneSelectHint生成的文案)
  208 + */
  209 + public AiResponseDTO initSceneGuide(String systemText,String sUserId,String sUserType,String authorization) {
  210 + try {
  211 + UserSceneSession userSceneSession = userSceneSessionService.getUserSceneSession( sUserId, sUserType,authorization);
  212 + systemText = userSceneSession.buildSceneSelectHint();
  213 + } catch (Exception e) {
  214 + systemText = "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>";
  215 + }
  216 + return AiResponseDTO.builder().aiText(StrUtil.EMPTY).systemText(systemText) .build();
  217 + }
  218 +
  219 +
  220 +
  221 +
  222 + // ====================== 动态构建Agent(支持选定场景/未选场景) ======================
  223 + private DynamicTableNl2SqlAiAgent createDynamicTableNl2SqlAiAgent(String userId, String userInput, UserSceneSession session) {
  224 +// 4. 获取/创建用DynamicTableNl2SqlAiAgent
  225 + DynamicTableNl2SqlAiAgent aiAgent = UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.get(userId);
  226 + if(ObjectUtil.isEmpty(aiAgent)){
  227 + aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class)
  228 + .chatLanguageModel(sqlChatModel)
  229 + .chatMemoryProvider(operableChatMemoryProvider)
  230 + .toolProvider(dynamicToolProvider)
  231 + .build();
  232 + UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent);
  233 + }
  234 + return aiAgent;
  235 + }
  236 +
  237 + // ====================== 动态构建Agent(支持选定场景/未选场景) ======================
  238 + private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) {
  239 +
  240 + // 1. 已选场景:强制绑定该场景工具
  241 + if (session.isSceneSelected() && session.getCurrentScene() != null) {
  242 + dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId());
  243 + } else {
  244 + // 2. 未选场景:大模型根据输入返加相应的场景
  245 + SceneDto sceneDto = parseSceneByLlm(userId, userInput, session);
  246 + if (sceneDto != null) {
  247 + session.setCurrentScene(sceneDto);
  248 + session.setSceneSelected(true);
  249 + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
  250 + dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId());
  251 + }else {return null;}
  252 + }
  253 + // 4. 获取/创建用Agent
  254 + ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId);
  255 + if(ObjectUtil.isEmpty(aiAgent)){
  256 + aiAgent = AiServices.builder(ErpAiAgent.class)
  257 + .chatLanguageModel(chatModel)
  258 + .chatMemoryProvider(operableChatMemoryProvider)
  259 + .toolProvider(dynamicToolProvider)
  260 + .build();
  261 + UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent);
  262 + log.info("用户{}Agent构建完成,已选场景:{},场景ID{}",
  263 + userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId));
  264 + }
  265 + return aiAgent;
  266 + }
  267 +
  268 +
  269 + /**
  270 + * 大模型意图解析核心方法(获取场景)
  271 + * @param userId 用户ID
  272 + * @param userInput 用户输入
  273 + * @param session 用户会话
  274 + * @return 匹配的BusinessScene,null表示解析失败
  275 + */
  276 + private SceneDto parseSceneByLlm(String userId, String userInput, UserSceneSession session) {
  277 + try {
  278 + List<ToolMeta> metasAll = session.getAuthTool();
  279 + // toolMetaMapper.findAll();
  280 + // 1. 构建大模型意图解析请求
  281 + String authScenesDesc =session.buildAuthScenesForLlm(metasAll);
  282 + // 2. 调用大模型解析意图,LangChain4j自动将大模型输出映射为SceneIntentParseResp
  283 +// {{authScenesDesc}}
  284 + SceneIntentParseResp parseResp = sceneSelectorAiAgent.parseSceneIntent(userInput,authScenesDesc);
  285 +// authScenesDesc
  286 + // 3. 解析结果处理
  287 + if (parseResp == null || parseResp.getSceneCode() == null || "NO_MATCH".equals(parseResp.getSceneCode())) {
  288 + log.warn("用户{}大模型未匹配到任何场景,输入:{}", userId, userInput);
  289 + return null;
  290 + }
  291 + // 4. 将场景编码转换为BusinessScene枚举
  292 + String sSceneNo = parseResp.getSceneCode();
  293 + return AppStartupRunner.getAiAgentByCode(sSceneNo);
  294 + } catch (Exception e) {
  295 + log.error("用户{}大模型意图解析失败,输入:{}", userId, userInput, e);
  296 + return null;
  297 + }
  298 + }
  299 +
  300 + /***
  301 + * @Author 钱豹
  302 + * @Date 19:28 2026/1/26
  303 + * @Param [userId, session]
  304 + * @return java.lang.String
  305 + * @Description 重置用户场景选择:恢复为未选状态,清空当前场景,重新展示选择界面
  306 + **/
  307 + private String resetUserScene(String userId, UserSceneSession session) {
  308 + session.setSceneSelected(false);
  309 + session.setBCleanMemory(false);
  310 + session.setCurrentTool(null);
  311 + session.setCurrentScene(null);
  312 + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
  313 + // 清空Agent缓存
  314 + UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
  315 + UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);
  316 + return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint();
  317 + }
  318 +
  319 + /**
  320 + * 处理用户场景选择:输入序号→匹配场景→更新会话状态
  321 + */
  322 + private AiResponseDTO handleSceneSelect(String userId, String userInput, UserSceneSession session) {
  323 + // 1. 尝试根据序号匹配场景
  324 + boolean selectSuccess = session.selectSceneByInput(userInput);
  325 + if (selectSuccess) {
  326 + // 2. 选择成功:更新缓存,返回成功提示
  327 + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
  328 + // 清空该用户原有Agent缓存(重新构建绑定新场景的Agent)
  329 + UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
  330 + //清除记忆缓存
  331 + operableChatMemoryProvider.clearSpecifiedMemory(userId);
  332 + String aiText = "智能体选择成功! 现在可以问她相关问题(如" + String.join("、", session.getCurrentScene().getSSceneContext()) + ")";
  333 + return AiResponseDTO.builder().aiText(aiText).sSceneName(session.getCurrentScene().getSSceneName()).build();
  334 + } else {
  335 + // 3. 选择失败:重新展示场景选择提示
  336 + return AiResponseDTO.builder().aiText(session.buildSceneSelectHint()).build();
  337 + }
  338 + }
  339 +
  340 + /***
  341 + * @Author 钱豹
  342 + * @Date 13:32 2026/2/6
  343 + * @Param [input, session]
  344 + * @return java.lang.String
  345 + * @Description 获取智普通智能体
  346 + **/
  347 + private AiResponseDTO getChatiAgent (String input,UserSceneSession session){
  348 + ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId());
  349 + if(ObjectUtil.isEmpty(chatiAgent)){
  350 + chatiAgent = AiServices.builder(ChatiAgent.class)
  351 + .chatLanguageModel(chatiModel)
  352 + .chatMemoryProvider(operableChatMemoryProvider)
  353 + .build();
  354 + UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); }
  355 + String sChatMessage = chatiAgent.chat(session.getUserId(), input);
  356 + String systemText = " 你已进入「随便聊聊」专栏, 可"+CommonConstant.RESET;
  357 + return AiResponseDTO.builder().aiText(sChatMessage).sSceneName("随便聊聊").systemText(systemText).sReturnType(ReturnTypeCode.HTML.getCode()).build();
  358 + }
  359 +
  360 +}
0 361 \ No newline at end of file
... ...