Commit 626352186da30dab810b1e319b83a7991d3166b8

Authored by zichun
0 parents

Initial commit: xlyAi ERP AI service (Spring Boot + LangChain4j)

Showing 144 changed files with 20523 additions and 0 deletions

Too many changes to show.

To preserve performance only 68 of 144 files are displayed.

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