完成基本功能

This commit is contained in:
2025-02-26 17:47:34 +08:00
parent 57ecdb62fa
commit ce59d63412
14 changed files with 1230 additions and 259 deletions

6
.idea/ApifoxUploaderProjectSetting.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-0ZZaS4q0gUiFOlbBJMN8hAmS7viQNi4D" />
</component>
</project>

17
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="main@localhost" uuid="4c84d2e6-c5f7-42bf-929e-5eb5cb744ec5">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3307/main</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

6
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

View File

@@ -9,12 +9,15 @@
"preview": "vite preview"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"chart.js": "^4.4.8",
"echarts": "^5.6.0",
"eventsource-client": "^1.1.3",
"licia": "^1.46.0",
"markdown-it": "^14.1.0",
"markdown-it-container": "^4.0.0",
"mermaid": "^11.4.1",
"mermaid-it-markdown": "^1.0.8",
"vue": "^3.5.13",
"vue3-markdown-it": "^1.0.10"
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",

212
client/pnpm-lock.yaml generated
View File

@@ -8,12 +8,24 @@ importers:
.:
dependencies:
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
chart.js:
specifier: ^4.4.8
version: 4.4.8
echarts:
specifier: ^5.6.0
version: 5.6.0
eventsource-client:
specifier: ^1.1.3
version: 1.1.3
licia:
specifier: ^1.46.0
version: 1.46.0
markdown-it:
specifier: ^14.1.0
version: 14.1.0
markdown-it-container:
specifier: ^4.0.0
version: 4.0.0
mermaid:
specifier: ^11.4.1
version: 11.4.1
@@ -23,9 +35,6 @@ importers:
vue:
specifier: ^3.5.13
version: 3.5.13
vue3-markdown-it:
specifier: ^1.0.10
version: 1.0.10(@types/markdown-it@14.1.2)
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.2.1
@@ -387,12 +396,12 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==, tarball: https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz}
'@mermaid-js/parser@0.3.0':
resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==}
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
@@ -623,15 +632,6 @@ packages:
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -754,6 +754,10 @@ packages:
caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
chart.js@4.4.8:
resolution: {integrity: sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==, tarball: https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz}
engines: {pnpm: '>=8'}
chevrotain-allstar@0.3.1:
resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
peerDependencies:
@@ -1002,6 +1006,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts@5.6.0:
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
editorconfig@1.0.4:
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
engines: {node: '>=14'}
@@ -1016,9 +1023,6 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
entities@2.1.0:
resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==}
entities@3.0.1:
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
engines: {node: '>=0.12'}
@@ -1042,6 +1046,14 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
eventsource-client@1.1.3:
resolution: {integrity: sha512-6GJGePDMxin/6VH1Dex7RqnZRguweO1BVKhXpBYwKcQbVLOZPLfeDr9vYSb9ra88kjs0NpT8FaEDz5rExedDkg==, tarball: https://registry.npmjs.org/eventsource-client/-/eventsource-client-1.1.3.tgz}
engines: {node: '>=18.0.0'}
eventsource-parser@3.0.0:
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==, tarball: https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz}
engines: {node: '>=18.0.0'}
execa@9.5.2:
resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==}
engines: {node: ^18.19.0 || >=20.5.0}
@@ -1203,8 +1215,8 @@ packages:
layout-base@2.0.1:
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
linkify-it@3.0.3:
resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==}
licia@1.46.0:
resolution: {integrity: sha512-Zms2AjJB+KdqUKFF87J5J/w9DwXnGN/lKlbjpRgvaPf0BIQ0mOZ/2lX4E79zwNafHGMUq5RtN54FN6Af5G92cA==}
linkify-it@4.0.1:
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
@@ -1219,9 +1231,6 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.flow@3.5.0:
resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -1231,48 +1240,8 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
markdown-it-abbr@1.0.4:
resolution: {integrity: sha512-ZeA4Z4SaBbYysZap5iZcxKmlPL6bYA8grqhzJIHB1ikn7njnzaP8uwbtuXc4YXD5LicI4/2Xmc0VwmSiFV04gg==}
markdown-it-anchor@8.6.7:
resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==}
peerDependencies:
'@types/markdown-it': '*'
markdown-it: '*'
markdown-it-deflist@2.1.0:
resolution: {integrity: sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==}
markdown-it-emoji@2.0.2:
resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==}
markdown-it-footnote@3.0.3:
resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==}
markdown-it-highlightjs@3.6.0:
resolution: {integrity: sha512-ex+Lq3cVkprh0GpGwFyc53A/rqY6GGzopPCG1xMsf8Ya3XtGC8Uw9tChN1rWbpyDae7tBBhVHVcMM29h4Btamw==}
markdown-it-ins@3.0.1:
resolution: {integrity: sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw==}
markdown-it-mark@3.0.1:
resolution: {integrity: sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A==}
markdown-it-sub@1.0.0:
resolution: {integrity: sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q==}
markdown-it-sup@1.0.0:
resolution: {integrity: sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==}
markdown-it-task-lists@2.1.1:
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
markdown-it-toc-done-right@4.2.0:
resolution: {integrity: sha512-UB/IbzjWazwTlNAX0pvWNlJS8NKsOQ4syrXZQ/C72j+jirrsjVRT627lCaylrKJFBQWfRsPmIVQie8x38DEhAQ==}
markdown-it@12.3.2:
resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==}
hasBin: true
markdown-it-container@4.0.0:
resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==}
markdown-it@13.0.2:
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
@@ -1527,6 +1496,9 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
@@ -1640,9 +1612,6 @@ packages:
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
vue3-markdown-it@1.0.10:
resolution: {integrity: sha512-mTvHu0zl7jrh7ojgaZ+tTpCLiS4CVg4bTgTu4KGhw/cRRY5YgIG8QgFAPu6kCzSW6Znc9a52Beb6hFvF4hSMkQ==}
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies:
@@ -1676,6 +1645,9 @@ packages:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
snapshots:
'@ampproject/remapping@2.3.0':
@@ -2021,12 +1993,12 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@kurkle/color@0.3.4': {}
'@mermaid-js/parser@0.3.0':
dependencies:
langium: 3.0.0
'@microsoft/fetch-event-source@2.0.1': {}
'@one-ini/wasm@0.1.1': {}
'@pkgjs/parseargs@0.11.0':
@@ -2224,15 +2196,6 @@ snapshots:
'@types/geojson@7946.0.16': {}
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@2.0.0': {}
'@types/trusted-types@2.0.7':
optional: true
@@ -2396,6 +2359,10 @@ snapshots:
caniuse-lite@1.0.30001700: {}
chart.js@4.4.8:
dependencies:
'@kurkle/color': 0.3.4
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
dependencies:
chevrotain: 11.0.3
@@ -2664,6 +2631,11 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts@5.6.0:
dependencies:
tslib: 2.3.0
zrender: 5.6.1
editorconfig@1.0.4:
dependencies:
'@one-ini/wasm': 0.1.1
@@ -2677,8 +2649,6 @@ snapshots:
emoji-regex@9.2.2: {}
entities@2.1.0: {}
entities@3.0.1: {}
entities@4.5.0: {}
@@ -2717,6 +2687,12 @@ snapshots:
estree-walker@2.0.2: {}
eventsource-client@1.1.3:
dependencies:
eventsource-parser: 3.0.0
eventsource-parser@3.0.0: {}
execa@9.5.2:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
@@ -2862,9 +2838,7 @@ snapshots:
layout-base@2.0.1: {}
linkify-it@3.0.3:
dependencies:
uc.micro: 1.0.6
licia@1.46.0: {}
linkify-it@4.0.1:
dependencies:
@@ -2881,8 +2855,6 @@ snapshots:
lodash-es@4.17.21: {}
lodash.flow@3.5.0: {}
lru-cache@10.4.3: {}
lru-cache@5.1.1:
@@ -2893,43 +2865,7 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
markdown-it-abbr@1.0.4: {}
markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@12.3.2):
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 12.3.2
markdown-it-deflist@2.1.0: {}
markdown-it-emoji@2.0.2: {}
markdown-it-footnote@3.0.3: {}
markdown-it-highlightjs@3.6.0:
dependencies:
highlight.js: 11.11.1
lodash.flow: 3.5.0
markdown-it-ins@3.0.1: {}
markdown-it-mark@3.0.1: {}
markdown-it-sub@1.0.0: {}
markdown-it-sup@1.0.0: {}
markdown-it-task-lists@2.1.1: {}
markdown-it-toc-done-right@4.2.0: {}
markdown-it@12.3.2:
dependencies:
argparse: 2.0.1
entities: 2.1.0
linkify-it: 3.0.3
mdurl: 1.0.1
uc.micro: 1.0.6
markdown-it-container@4.0.0: {}
markdown-it@13.0.2:
dependencies:
@@ -3208,6 +3144,8 @@ snapshots:
ts-dedent@2.2.0: {}
tslib@2.3.0: {}
uc.micro@1.0.6: {}
uc.micro@2.1.0: {}
@@ -3303,24 +3241,6 @@ snapshots:
vscode-uri@3.0.8: {}
vue3-markdown-it@1.0.10(@types/markdown-it@14.1.2):
dependencies:
markdown-it: 12.3.2
markdown-it-abbr: 1.0.4
markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@12.3.2)
markdown-it-deflist: 2.1.0
markdown-it-emoji: 2.0.2
markdown-it-footnote: 3.0.3
markdown-it-highlightjs: 3.6.0
markdown-it-ins: 3.0.1
markdown-it-mark: 3.0.1
markdown-it-sub: 1.0.0
markdown-it-sup: 1.0.0
markdown-it-task-lists: 2.1.1
markdown-it-toc-done-right: 4.2.0
transitivePeerDependencies:
- '@types/markdown-it'
vue@3.5.13:
dependencies:
'@vue/compiler-dom': 3.5.13
@@ -3353,3 +3273,7 @@ snapshots:
yallist@3.1.1: {}
yoctocolors@2.1.1: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

View File

@@ -1,73 +1,161 @@
<script setup>
import {ref} from 'vue'
import {onMounted, ref} from 'vue'
import MarkdownIt from 'markdown-it'
import MarkDownMermaidPlugin from 'mermaid-it-markdown'
import {fetchEventSource} from '@microsoft/fetch-event-source'
import {createEventSource} from 'eventsource-client'
// 初始化 markdown-it 实例
const md = new MarkdownIt({
html: true, // 启用 HTML 标签
breaks: true, // 转换换行符为 <br>
linkify: true, // 自动转换 URL 为链接
typographer: true // 启用一些语言中性的替换 + 引号美化
html: true, // 启用 HTML 标签
breaks: true, // 转换换行符为 <br>
linkify: true, // 自动转换 URL 为链接
typographer: true, // 启用一些语言中性的替换 + 引号美化
})
md.use(MarkDownMermaidPlugin)
const question = ref('')
const answer = ref('')
const loading = ref(false)
const useTw = ref(false)
// 使用计算属性来渲染 markdown
const renderedAnswer = ref('')
answer.value = "```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```"
renderedAnswer.value = md.render(answer.value)
// 在 script setup 中添加新的响应式变量
const useDatabase = ref(false)
const databaseUrl = ref('')
const databaseUsername = ref('')
const databasePassword = ref('')
const databaseName = ref('')
// 在 script setup 中添加 Toast 相关的响应式变量
const showToast = ref(false)
const toastMessage = ref('')
const toastMessageType = ref('success')
const handleSubmit = async () => {
if (!question.value.trim() || loading.value) return
loading.value = true
answer.value = ''
renderedAnswer.value = ''
try {
await fetchEventSource('http://localhost:7891/chat/stream', {
const url = 'http://localhost:7891/chat/stream'
const formData = new FormData()
formData.append('prompt', question.value.trim())
formData.append('use_tw', useTw.value)
// 如果启用数据库,添加相关参数
if (useDatabase.value) {
formData.append('use_database', useDatabase.value)
formData.append('database_url', databaseUrl.value)
formData.append('database_username', databaseUsername.value)
formData.append('database_password', databasePassword.value)
formData.append('database_name', databaseName.value)
}
const eventSource = createEventSource({
url: url,
method: 'POST',
body: question.value.trim(),
onmessage(event) {
// 处理每个消息事件
answer.value += event.data
// 实时渲染 markdown
renderedAnswer.value = md.render(answer.value)
},
onclose() {
loading.value = false
body: formData,
onDisconnect: () => {
console.log(answer.value)
console.log(renderedAnswer.value)
eventSource.close()
},
onerror(err) {
console.error('请求出错:', err)
loading.value = false
return false // 不进行重试
}
})
for await (const { data } of eventSource) {
answer.value += data.replaceAll(/#\/#/g, '')
renderedAnswer.value = md.render(answer.value)
}
} catch (error) {
console.error('请求出错:', error)
answer.value = '抱歉,请求出现错误'
renderedAnswer.value = answer.value
showToastMessage('请求失败,请检查服务器连接', 'error')
} finally {
loading.value = false
}
}
// 添加设置对话框的状态控制
const showSettings = ref(false)
// 添加保存设置到本地存储的函数
const saveSettingsToStorage = () => {
const settings = {
useTw: useTw.value,
useDatabase: useDatabase.value,
databaseUrl: databaseUrl.value,
databaseUsername: databaseUsername.value,
databasePassword: databasePassword.value,
databaseName: databaseName.value,
}
localStorage.setItem('chatSettings', JSON.stringify(settings))
}
// 从本地存储加载设置的函数
const loadSettingsFromStorage = () => {
const settings = localStorage.getItem('chatSettings')
if (settings) {
const parsed = JSON.parse(settings)
useTw.value = parsed.useTw
useDatabase.value = parsed.useDatabase
databaseUrl.value = parsed.databaseUrl
databaseUsername.value = parsed.databaseUsername
databasePassword.value = parsed.databasePassword
databaseName.value = parsed.databaseName || ''
}
}
// 修改 showToastMessage 函数,添加消息类型参数
const showToastMessage = (message, type = 'success') => {
toastMessage.value = message
toastMessageType.value = type
showToast.value = true
setTimeout(() => {
showToast.value = false
}, 3000)
}
// 修改 handleCloseSettings 函数中的调用
const handleCloseSettings = () => {
if (useDatabase.value) {
if (!databaseUrl.value.trim()) {
showToastMessage('请输入数据库地址', 'error')
return
}
if (!databaseUsername.value.trim()) {
showToastMessage('请输入数据库用户名', 'error')
return
}
if (!databaseName.value.trim()) {
showToastMessage('请输入数据库名称', 'error')
return
}
}
saveSettingsToStorage()
showSettings.value = false
showToastMessage('设置已保存', 'success')
}
// 在组件挂载时加载设置
onMounted(() => {
loadSettingsFromStorage()
})
</script>
<template>
<div class="chat-container">
<div class="chat-header">
<h1>AI 助手</h1>
<h1>数据库助手</h1>
</div>
<div class="messages-container">
<div class="answer-area">
<div
<div
v-if="renderedAnswer"
class="markdown-body"
v-html="renderedAnswer"
@@ -80,7 +168,7 @@ const handleSubmit = async () => {
</div>
</template>
<template v-else>
在下方输入您的问题AI 助手会为您解答...
你好很高兴见到你有什么我可以帮忙的吗
</template>
</div>
</div>
@@ -88,21 +176,176 @@ const handleSubmit = async () => {
<div class="input-area">
<div class="input-container">
<textarea
<textarea
v-model="question"
placeholder="请输入您的问题... (Ctrl + Enter 快速发送)"
@keyup.ctrl.enter="handleSubmit"
></textarea>
<button
:disabled="loading"
class="send-button"
@click="handleSubmit"
>
<span v-if="loading" class="loading-spinner"></span>
<span>{{ loading ? '思考中...' : '发送' }}</span>
</button>
<div class="button-group">
<button :disabled="loading" class="send-button" @click="handleSubmit">
<template v-if="loading">
<span class="loading-spinner"></span>
</template>
<template v-else>
<span>发送</span>
</template>
</button>
<button class="settings-button" @click="showSettings = true">
<span>设置</span>
</button>
</div>
</div>
</div>
<!-- 修改设置对话框的 Transition 组件 -->
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="showSettings" class="settings-modal">
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 translate-y-4"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-4"
>
<div class="settings-content">
<div class="settings-header">
<h2>设置</h2>
</div>
<div class="settings-body">
<label class="switch-wrapper">
<span class="switch-label">图文并茂</span>
<div class="switch">
<input v-model="useTw" type="checkbox" />
<span class="slider"></span>
</div>
</label>
<label class="switch-wrapper">
<span class="switch-label">连接数据库</span>
<div class="switch">
<input v-model="useDatabase" type="checkbox" />
<span class="slider"></span>
</div>
</label>
<!-- 数据库设置项仅在 useDatabase true 时显示 -->
<div v-if="useDatabase" class="database-settings">
<div class="input-field">
<label for="database-url">数据库地址</label>
<div class="input-wrapper">
<input
id="database-url"
v-model="databaseUrl"
placeholder="请输入数据库连接地址"
type="text"
/>
<button
v-if="databaseUrl"
class="clear-button"
@click="databaseUrl = ''"
>
</button>
</div>
</div>
<div class="input-field">
<label for="database-username">用户名</label>
<div class="input-wrapper">
<input
id="database-username"
v-model="databaseUsername"
placeholder="请输入数据库用户名"
type="text"
/>
<button
v-if="databaseUsername"
class="clear-button"
@click="databaseUsername = ''"
>
</button>
</div>
</div>
<div class="input-field">
<label for="database-password">密码</label>
<div class="input-wrapper">
<input
id="database-password"
v-model="databasePassword"
placeholder="请输入数据库密码"
type="password"
/>
<button
v-if="databasePassword"
class="clear-button"
@click="databasePassword = ''"
>
</button>
</div>
</div>
<div class="input-field">
<label for="database-name">数据库名</label>
<div class="input-wrapper">
<input
id="database-name"
v-model="databaseName"
placeholder="请输入数据库名称"
type="text"
/>
<button
v-if="databaseName"
class="clear-button"
@click="databaseName = ''"
>
</button>
</div>
</div>
</div>
</div>
<div class="settings-footer">
<button class="save-button" @click="handleCloseSettings">
<svg
fill="none"
height="20"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
<span>保存</span>
</button>
</div>
</div>
</Transition>
</div>
</Transition>
<!-- 修改 Toast 组件 -->
<div v-if="showToast" :data-type="toastMessageType" class="toast-message">
{{ toastMessage }}
</div>
</div>
</template>
@@ -112,13 +355,17 @@ const handleSubmit = async () => {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fafafa; /* 更柔和的背景色 */
background-color: #f8fafc; /* 更的背景色 */
}
.chat-header {
padding: 20px 32px;
background: linear-gradient(to right, #2563eb, #3b82f6); /* 渐变背景 */
box-shadow: 0 2px 12px rgba(37, 99, 235, 0.15);
background: linear-gradient(
to right,
#334155,
#475569
); /* 更深沉的渐变背景 */
box-shadow: 0 2px 12px rgba(51, 65, 85, 0.15);
position: sticky;
top: 0;
z-index: 10;
@@ -136,7 +383,11 @@ const handleSubmit = async () => {
flex: 1;
overflow-y: auto;
padding: 32px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); /* 渐变背景 */
background: linear-gradient(
135deg,
#f1f5f9 0%,
#ffffff 100%
); /* 更柔和的渐变背景 */
scroll-behavior: smooth;
}
@@ -144,12 +395,13 @@ const handleSubmit = async () => {
max-width: 1200px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 20px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
padding: 40px;
min-height: 200px;
min-height: 200px; /* 保持最小高度 */
height: auto; /* 添加自适应高度 */
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
}
.placeholder {
@@ -162,13 +414,15 @@ const handleSubmit = async () => {
}
.input-area {
padding: 24px 32px;
padding: 16px 24px; /* 减小内边距 */
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(0, 0, 0, 0.06);
position: sticky;
bottom: 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
}
.input-container {
@@ -176,17 +430,16 @@ const handleSubmit = async () => {
margin: 0 auto;
width: 100%;
display: flex;
gap: 20px;
gap: 12px; /* 减小间距 */
}
textarea {
flex: 1;
min-height: 64px;
max-height: 200px;
padding: 18px 24px;
border: 2px solid #e2e8f0;
border-radius: 16px;
resize: none; /* 禁用调整大小功能 */
padding: 14px 20px; /* 减小内边距 */
border: 1px solid #e2e8f0;
border-radius: 12px;
resize: none;
font-size: 1rem;
line-height: 1.6;
transition: all 0.2s ease;
@@ -196,8 +449,8 @@ textarea {
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
border-color: #475569;
box-shadow: 0 0 0 4px rgba(71, 85, 105, 0.15);
}
textarea::placeholder {
@@ -205,31 +458,42 @@ textarea::placeholder {
font-weight: 500;
}
.send-button {
padding: 0 32px;
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
.button-group {
display: flex;
flex-direction: column;
gap: 6px; /* 减小按钮之间的间距 */
justify-content: stretch;
}
.send-button,
.settings-button {
width: 100px; /* 统一按钮宽度 */
height: 46px; /* 统一按钮高度 */
background: linear-gradient(135deg, #334155 0%, #475569 100%);
color: white;
border: none;
border-radius: 16px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
box-shadow: 0 4px 12px rgba(51, 65, 85, 0.2);
}
.send-button:hover:not(:disabled) {
.send-button:hover:not(:disabled),
.settings-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.25);
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
box-shadow: 0 6px 20px rgba(51, 65, 85, 0.25);
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
}
.send-button:active:not(:disabled) {
.send-button:active:not(:disabled),
.settings-button:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.2);
box-shadow: 0 2px 8px rgba(51, 65, 85, 0.2);
}
.send-button:disabled {
@@ -241,7 +505,11 @@ textarea::placeholder {
/* Markdown 样式优化 */
.markdown-body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
width: 100%;
height: auto; /* 添加自适应高度 */
overflow: visible; /* 确保内容不会被截断 */
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
font-size: 1.05rem;
line-height: 1.8;
color: #334155;
@@ -249,20 +517,21 @@ textarea::placeholder {
.markdown-body :deep(pre) {
background-color: #f8fafc;
border-radius: 16px;
padding: 24px;
border-radius: 8px; /* 减小圆角 */
padding: 20px;
overflow-x: auto;
border: 1px solid #e2e8f0;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.03);
}
.markdown-body :deep(code) {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, monospace;
font-size: 0.95em;
background-color: #f1f5f9;
padding: 0.2em 0.4em;
border-radius: 6px;
color: #2563eb;
border-radius: 4px;
color: #475569;
}
.markdown-body :deep(pre code) {
@@ -275,9 +544,9 @@ textarea::placeholder {
.markdown-body :deep(blockquote) {
margin: 2em 0;
padding: 1em 1.5em;
border-left: 4px solid #3b82f6;
border-left: 4px solid #475569;
background-color: #f8fafc;
border-radius: 0 16px 16px 0;
border-radius: 0 8px 8px 0; /* 减小圆角 */
color: #475569;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
}
@@ -290,7 +559,7 @@ textarea::placeholder {
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
display: block; /* 改为 block 以确保居中 */
}
@keyframes spin {
@@ -301,25 +570,17 @@ textarea::placeholder {
/* 响应式设计优化 */
@media (max-width: 768px) {
.messages-container {
padding: 20px;
}
.answer-area {
padding: 24px;
}
.input-area {
padding: 16px 20px;
padding: 12px 16px; /* 移动端进一步减小内边距 */
}
.input-container {
gap: 8px; /* 移动端减小间距 */
}
textarea {
padding: 16px 20px;
font-size: 16px;
}
.send-button {
padding: 0 24px;
min-height: 56px; /* 移动端减小文本框高度 */
padding: 12px 16px; /* 移动端减小内边距 */
}
}
@@ -330,16 +591,346 @@ textarea::placeholder {
justify-content: center;
gap: 12px;
font-size: 1.2rem;
color: #3b82f6;
color: #475569; /* 更改思考中的颜色 */
}
.thinking-spinner {
width: 16px;
height: 16px;
border: 2.5px solid #3b82f6;
border: 2.5px solid #475569; /* 更改加载动画颜色 */
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
}
</style>
.settings-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-start;
justify-content: center;
z-index: 1000;
padding-top: 15vh;
}
.settings-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 600px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
transform-origin: bottom;
}
.settings-header {
padding: 5px 20px;
border-bottom: 1px solid #e2e8f0;
}
.settings-body {
padding: 24px;
flex: 1;
min-height: 200px;
}
.settings-footer {
padding: 10px 20px;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
background-color: #f8fafc;
border-radius: 0 0 12px 12px;
}
.save-button {
background: #334155;
border: none;
cursor: pointer;
padding: 8px 15px;
color: white;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.2s ease;
}
.save-button:hover {
background: #1e293b;
transform: translateY(-1px);
}
.save-button:active {
transform: translateY(0);
}
/* 修改数据库设置区域的样式 */
.database-settings {
margin-top: 12px;
padding: 12px 16px;
background-color: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.input-field {
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.input-field:last-child {
margin-bottom: 0;
}
.input-field label {
display: block;
font-size: 1rem;
color: #334155;
font-weight: 500;
flex: 0 0 80px;
}
.input-field input {
flex: 1;
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 0.95rem;
transition: all 0.2s ease;
background-color: white;
padding-right: 32px; /* 为清空按钮留出空间 */
}
.input-field input:focus {
outline: none;
border-color: #475569;
box-shadow: 0 0 0 2px rgba(71, 85, 105, 0.1);
}
.input-field input::placeholder {
color: #94a3b8;
}
.input-wrapper {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
.clear-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
border-radius: 50%;
width: 20px;
height: 20px;
transition: all 0.2s ease;
z-index: 1;
}
.clear-button:hover {
background-color: #f1f5f9;
color: #64748b;
}
.clear-button:active {
background-color: #e2e8f0;
}
/* 优化开关选项的间距 */
.switch-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.switch-wrapper + .switch-wrapper {
border-top: 1px solid #e2e8f0;
}
.switch-label {
font-size: 1rem;
color: #334155;
font-weight: 500;
}
/* 开关样式 */
.switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #cbd5e1;
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: '';
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #334155;
}
input:focus + .slider {
box-shadow: 0 0 1px #334155;
}
input:checked + .slider:before {
transform: translateX(24px);
}
/* 修改 Toast 样式 */
.toast-message {
position: fixed;
top: 90px;
left: 50%;
transform: translateX(-50%);
background-color: #334155;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
z-index: 2000;
display: flex;
align-items: center;
gap: 8px;
}
.toast-message::before {
font-weight: bold;
}
/* 成功状态的 toast */
.toast-message[data-type='success']::before {
content: '✓';
color: #10b981;
}
/* 错误状态的 toast */
.toast-message[data-type='error']::before {
content: '!';
color: #ef4444;
}
/* 添加响应式样式 */
@media (max-width: 768px) {
.toast-message {
width: 90%;
max-width: 320px;
text-align: center;
justify-content: center;
}
}
/* 添加必要的过渡类 */
.transition {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.duration-300 {
transition-duration: 300ms;
}
.duration-200 {
transition-duration: 200ms;
}
.ease-out {
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
.ease-in {
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
.opacity-0 {
opacity: 0;
}
.opacity-100 {
opacity: 1;
}
.translate-y-4 {
transform: translateY(1rem);
}
.translate-y-0 {
transform: translateY(0);
}
/* 删除这些不需要的类 */
.translate-y-full {
transform: translateY(100%);
}
.translate-y-\[-20px\] {
transform: translateY(-20px);
}
/* 添加图表容器样式 */
.chart-container {
width: 100%;
max-width: 800px;
margin: 20px auto;
padding: 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
</style>

View File

@@ -1,7 +1,5 @@
import {createApp} from 'vue'
import App from './App.vue'
import Markdown from 'vue3-markdown-it'
const app = createApp(App)
.use(Markdown)
.mount('#app')

14
pom.xml
View File

@@ -22,9 +22,21 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!--<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@@ -1,15 +1,23 @@
package com.lanyuanxiaoyao.ai.analysis.controller;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.ai.analysis.tools.DatabaseTools;
import com.lanyuanxiaoyao.ai.analysis.tools.DatetimeTools;
import com.lanyuanxiaoyao.ai.analysis.tools.PromptTools;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Flux;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@Slf4j
@CrossOrigin
@@ -23,8 +31,17 @@ public class ChatController {
}
@ResponseBody
@PostMapping(value = "", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=utf-8")
public String chat(@RequestBody String prompt) {
@PostMapping(value = "", produces = MediaType.TEXT_PLAIN_VALUE)
public String chat(
@RequestParam("prompt") String prompt,
@RequestParam(value = "use_tw", defaultValue = "false") Boolean useTw,
@RequestParam(value = "use_database", defaultValue = "false") Boolean useDatabase,
@RequestParam(value = "database_url", required = false) String databaseUrl,
@RequestParam(value = "database_username", required = false) String databaseUsername,
@RequestParam(value = "database_password", required = false) String databasePassword,
@RequestParam(value = "database_name", required = false) String databaseName
) {
logParameters(prompt, useTw, useDatabase, databaseUrl, databaseUsername, databasePassword, databaseName);
return client.prompt()
.system("始终在中文语境下回答")
.user(prompt)
@@ -32,12 +49,65 @@ public class ChatController {
.content();
}
@PostMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=utf-8")
public Flux<String> chatStream(@RequestBody String prompt) {
return client.prompt()
.system("始终在中文语境下回答")
@PostMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(
@RequestParam("prompt") String prompt,
@RequestParam(value = "use_tw", defaultValue = "false") Boolean useTw,
@RequestParam(value = "use_database", defaultValue = "false") Boolean useDatabase,
@RequestParam(value = "database_url", required = false) String databaseUrl,
@RequestParam(value = "database_username", required = false) String databaseUsername,
@RequestParam(value = "database_password", required = false) String databasePassword,
@RequestParam(value = "database_name", required = false) String databaseName
) {
log.info("chatStream");
logParameters(prompt, useTw, useDatabase, databaseUrl, databaseUsername, databasePassword, databaseName);
SseEmitter emitter = new SseEmitter();
StringBuilder systemPrompt = new StringBuilder("始终在中文语境下回答\n\n");
systemPrompt
.append("当遇到数据对比、占比、趋势等场景相关的提问优先考虑使用mermaid绘制图表图文结合来回答")
.append(PromptTools.MERMAID_PROMPT)
.append("\n\n");
List<Object> tools = new ArrayList<>();
tools.add(new DatetimeTools());
if (useDatabase) {
log.info("use database");
DatabaseTools databaseTools = new DatabaseTools(databaseUrl, databaseUsername, databasePassword);
systemPrompt.append("以下是Hudi数据库中已有表的详细信息包含表名、表描述、字段名和字段描述任何与这些表数据相关的问题都优先查询数据库获取答案\n");
systemPrompt.append(databaseTools.getTableInformation(databaseName)).append("\n");
systemPrompt.append("任何使用Hudi数据库的数据的回答都必须从实际的数据中获取严禁虚构任何下列信息中不存在的表或表字段没有描述的表或字段可以从表名或字段名中推测如果找不到答案就回复从数据库中没有找到相关数据\n");
tools.add(databaseTools);
}
client.prompt()
.system(systemPrompt.toString())
.user(prompt)
.tools(tools.toArray(new Object[]{}))
.stream()
.content();
.content()
.subscribe(
text -> {
try {
emitter.send(SseEmitter.event().id(IdUtil.nanoId()).data(StrUtil.format("#/#{}#/#", text)).build());
} catch (IOException e) {
emitter.completeWithError(e);
}
},
emitter::completeWithError,
emitter::complete
);
return emitter;
}
private void logParameters(String prompt, Boolean useTw, Boolean useDatabase, String databaseUrl, String databaseUsername, String databasePassword, String databaseName) {
log.info("prompt: {}", prompt);
log.info("use_tw: {}", useTw);
log.info("use_database: {}", useDatabase);
if (useDatabase) {
log.info("database_url: {}", databaseUrl);
log.info("database_username: {}", databaseUsername);
log.info("database_password: {}", databasePassword);
log.info("database_name: {}", databaseName);
}
}
}

View File

@@ -0,0 +1,90 @@
package com.lanyuanxiaoyao.ai.analysis.tools;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.DbUtil;
import cn.hutool.db.Entity;
import cn.hutool.db.Session;
import cn.hutool.db.ds.simple.SimpleDataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
/**
* 提供AI调用数据库的能力
*
* @author lanyuanxiaoyao
* @version 20250225
*/
@Slf4j
public class DatabaseTools {
private final DataSource dataSource;
public DatabaseTools(String url, String username, String password) {
this.dataSource = new SimpleDataSource(url, username, password);
}
public static void main(String[] args) {
DatabaseTools tools = new DatabaseTools(
"jdbc:mysql://localhost:3307/main",
"test",
"test"
);
log.info(tools.getTableInformation("main"));
}
@Tool(description = "连接数据库执行SQL查询语句查询结果对应的数据并以CSV格式返回调用这个方法必须传如SQL语句作为参数")
public String query(@ToolParam(description = "标准MySQL语句不允许有除了SQL语句本身之外的任何提示性文字、注释、说明等") String sql) {
log.info("sql: {}", sql);
try (Session session = DbUtil.newSession(dataSource)) {
StringBuilder builder = new StringBuilder();
List<Entity> entities = session.query(sql);
for (Entity entity : entities) {
Set<String> fields = entity.getFieldNames();
for (String field : fields) {
builder.append(entity.get(field)).append(",");
}
builder.append("\n");
}
return builder.toString();
} catch (SQLException e) {
return StrUtil.format("查询失败,失败提示:{}", e.getMessage());
}
}
public String getTableInformation(String databaseName) {
StringBuilder builder = new StringBuilder();
try (Session session = DbUtil.newSession(dataSource)) {
try (Connection connection = session.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
ResultSet tables = metaData.getTables(databaseName, null, null, null);
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
String tableComment = tables.getString("REMARKS");
;
builder.append(StrUtil.format("以下是{}表的详细信息\n", tableName));
if (StrUtil.isNotBlank(tableComment)) {
builder.append(StrUtil.format("表描述:{}\n", tableComment));
}
ResultSet columns = metaData.getColumns(databaseName, null, tableName, null);
builder.append(StrUtil.format("以下是{}表的字段名和字段描述\n", tableName));
while (columns.next()) {
String columnName = columns.getString("COLUMN_NAME");
String columnComment = columns.getString("REMARKS");
builder.append(StrUtil.format("{}:{}", columnName, StrUtil.isBlank(columnComment) ? "" : columnComment)).append("\n");
}
builder.append("\n");
}
} catch (SQLException e) {
return StrUtil.format("获取表信息失败,失败提示:{}", e.getMessage());
}
}
return builder.toString();
}
}

View File

@@ -0,0 +1,24 @@
package com.lanyuanxiaoyao.ai.analysis.tools;
import java.time.LocalDateTime;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.function.FunctionToolCallback;
/**
* @author lanyuanxiaoyao
* @version 20250225
*/
@Slf4j
public class DatetimeTools {
public static final FunctionToolCallback<Void, LocalDateTime> call = FunctionToolCallback.builder("current_time", () -> LocalDateTime.now())
.description("返回当前的日期时间")
.inputType(Void.class)
.build();
@Tool(description = "返回当前的日期时间")
public String currentTime() {
return LocalDateTime.now().toString();
}
}

View File

@@ -0,0 +1,211 @@
package com.lanyuanxiaoyao.ai.analysis.tools;
/**
* @author lanyuanxiaoyao
* @version 20250225
*/
public class PromptTools {
public static final String MERMAID_PROMPT = """
### 概述
Mermaid 是一种基于 JavaScript 的工具,使用 Markdown 风格的文本生成各种图表。以下是为内网大模型准备的语法总结,确保模型能理解并生成正确的图表代码。我们将详细介绍每种图表类型的用途和基本语法,并提供示例。
### 详细语法
以下是 12 种 Mermaid 图表类型的语法总结,每个类型包括用途、语法规则和示例,方便模型识别和使用。
#### 流程图 (Flowchart)
- **用途**: 用于表示算法、工作流或流程。
- **语法**:
- 以 `graph [direction]` 开始,方向包括 `TB`(从上到下)、`BT`(从下到上)等。
- 节点用 ID 和标签定义,连接用箭头 `-->` 表示。
- **示例**:
```mermaid
graph TD
A[开始] --> B{条件}
B -- 是 --> C[处理]
B -- 否 --> D[结束]
```
#### 序列图 (Sequence Diagram)
- **用途**: 显示对象或参与者之间的交互。
- **语法**:
- 以 `sequenceDiagram` 开始。
- 定义参与者用 `participant <name>`。
- 消息用 `<sender>->><receiver>: message` 发送。
- **示例**:
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>Bob: 你好
Bob->>Alice: 嗨
```
#### 类图 (Class Diagram)
- **用途**: 描述系统结构,显示类及其关系。
- **语法**:
- 以 `classDiagram` 开始。
- 定义类用 `class <ClassName> { <attributes> <methods> }`。
- 关系用 `<ClassA> <relationship> <ClassB>` 表示(如 `extends`)。
- **示例**:
```mermaid
classDiagram
class Animal
class Dog extends Animal
class Cat extends Animal
```
#### 状态图 (State Diagram)
- **用途**: 通过状态和转换建模系统或对象的行为。
- **语法**:
- 以 `stateDiagram` 开始。
- 定义状态用 `state <StateName>`。
- 转换用 `<StateA> --> <StateB>: condition` 表示。
- **示例**:
```mermaid
stateDiagram
[*] --> Active
Active --> Inactive: 停用时
Inactive --> Active: 激活时
Active --> [*]: 销毁时
```
#### 实体关系图 (ER Diagram)
- **用途**: 可视化数据库中的实体关系。
- **语法**:
- 以 `erDiagram` 开始。
- 定义实体用 `<EntityName> { <attributes> }`。
- 关系用 `<EntityA> |<RelationshipType>| <EntityB>: cardinality` 表示。
- **示例**:
```mermaid
erDiagram
Customer { name }
Order { order_id }
Customer ||--o{ Order: 放置
```
#### 用户旅程图 (User Journey Diagram)
- **用途**: 映射用户完成任务的步骤。
- **语法**:
- 以 `journey` 开始。
- 定义部分用 `section <SectionName>`。
- 定义任务用 `<TaskName>: <score>: <actors>`。
- **示例**:
```mermaid
journey
title 我的工作日
section 去上班
泡茶: 5: 我
上楼: 3: 我
工作: 1: 我, 猫
section 回家
下楼: 5: 我
坐下: 5: 我
```
#### 甘特图 (Gantt Chart)
- **用途**: 安排和跟踪项目进度。
- **语法**:
- 以 `gantt` 开始。
- 定义部分和任务,包括日期和持续时间。
- **示例**:
```mermaid
gantt
dateFormat YYYY-MM-DD
section 部分
任务: done, 2020-01-01, 2020-01-02
活动任务: active, 2020-01-03, 3d
未来任务: 2020-01-06, 5d
```
#### 饼图 (Pie Chart)
- **用途**: 显示整体的比例或百分比。
- **语法**:
- 以 `pie` 开始。
- 定义切片用 `label: value`。
- **示例**:
```mermaid
pie
title 我的饼图
"标签1": 50
"标签2": 25
"标签3": 25
```
#### 需求图 (Requirement Diagram)
- **用途**: 可视化系统设计中的需求及其关系。
- **语法**:
- 以 `requirementDiagram` 开始。
- 定义需求用 `<type> <name> { id: <id>, text: <text>, risk: <risk>, verifymethod: <method> }`。
- 定义需求间关系。
- **示例**:
```mermaid
requirementDiagram
requirement Req1 { id: R1, text: "第一个需求" }
requirement Req2 { id: R2, text: "第二个需求" }
Req1 - satisfies -> Req2
```
#### Git 图 (Git Graph)
- **用途**: 展示 Git 仓库的历史。
- **语法**:
- 以 `gitGraph` 开始。
- 定义提交、分支和合并。
- **示例**:
```mermaid
gitGraph
commit
branch develop
commit
commit
merge master
```
#### 时间线 (Timeline)
- **用途**: 显示按时间顺序的事件。
- **语法**:
- 以 `timeline` 开始。
- 定义时间段和事件。
- **示例**:
```mermaid
timeline
title 互联网历史
1969 : ARPANET 诞生
1973 : 以太网发明
1983 : DNS 创建
```
#### 思维导图 (Mindmap)
- **用途**: 可视化想法及其关系。
- **语法**:
- 以 `mindmap` 开始。
- 用缩进定义层次结构。
- **示例**:
```mermaid
mindmap
根节点
子节点 1
孙节点 1
孙节点 2
子节点 2
```
---
#### 图表类型与语法详细分析
以下是每种图表类型的详细语法说明,包括用途、语法规则和示例。我们使用表格形式组织信息,以提高可读性。
| 图表类型 | 用途 | 语法规则 | 示例 |
|----------------|-------------------------------|------------------------------------------------------------------------|--------------------------------------------------------------------|
| 流程图 (Flowchart) | 表示算法、工作流或流程 | 以 `graph [direction]` 开始,方向如 `TB`、`LR`;节点用 ID 和标签定义,连接用 `-->`。 | `graph TD A[开始] --> B{条件} B -- 是 --> C[处理] B -- 否 --> D[结束]` |
| 序列图 (Sequence Diagram) | 显示对象或参与者间的交互 | 以 `sequenceDiagram` 开始,定义参与者用 `participant <name>`,消息用 `<sender>->><receiver>: message`。 | `sequenceDiagram participant Alice participant Bob Alice->>Bob: 你好 Bob->>Alice: 嗨` |
| 类图 (Class Diagram) | 描述系统结构,显示类及其关系 | 以 `classDiagram` 开始,定义类用 `class <ClassName> { <attributes> <methods> }`,关系用 `<ClassA> <relationship> <ClassB>`。 | `classDiagram class Animal class Dog extends Animal class Cat extends Animal` |
| 状态图 (State Diagram) | 通过状态和转换建模系统或对象行为 | 以 `stateDiagram` 开始,定义状态用 `state <StateName>`,转换用 `<StateA> --> <StateB>: condition`。 | `stateDiagram [*] --> Active Active --> Inactive: 停用时 Inactive --> Active: 激活时 Active --> [*]: 销毁时` |
| 实体关系图 (ER Diagram) | 可视化数据库中的实体关系 | 以 `erDiagram` 开始,定义实体用 `<EntityName> { <attributes> }`,关系用 `<EntityA> |<RelationshipType>| <EntityB>: cardinality`。 | `erDiagram Customer { name } Order { order_id } Customer ||--o{ Order: 放置` |
| 用户旅程图 (User Journey Diagram) | 映射用户完成任务的步骤 | 以 `journey` 开始,定义部分用 `section <SectionName>`,任务用 `<TaskName>: <score>: <actors>`。 | `journey title 我的工作日 section 去上班 泡茶: 5: 我 上楼: 3: 我 工作: 1: 我, 猫 section 回家 下楼: 5: 我 坐下: 5: 我` |
| 甘特图 (Gantt Chart) | 安排和跟踪项目进度 | 以 `gantt` 开始,定义部分和任务,包括日期和持续时间。 | `gantt dateFormat YYYY-MM-DD section 部分 任务: done, 2020-01-01, 2020-01-02 Active task: active, 2020-01-03, 3d Future task: 2020-01-06, 5d` |
| 饼图 (Pie Chart) | 显示整体的比例或百分比 | 以 `pie` 开始,定义切片用 `label: value`。 | `pie title 我的饼图 "标签1": 50 "标签2": 25 "标签3": 25` |
| 需求图 (Requirement Diagram) | 可视化系统设计中的需求及其关系 | 以 `requirementDiagram` 开始,定义需求用 `<type> <name> { id: <id>, text: <text>, risk: <risk>, verifymethod: <method> }`,定义关系。 | `requirementDiagram requirement Req1 { id: R1, text: "第一个需求" } requirement Req2 { id: R2, text: "第二个需求" } Req1 - satisfies -> Req2` |
| Git 图 (Git Graph) | 展示 Git 仓库的历史 | 以 `gitGraph` 开始,定义提交、分支和合并。 | `gitGraph commit branch develop commit commit merge master` |
| 时间线 (Timeline) | 显示按时间顺序的事件 | 以 `timeline` 开始,定义时间段和事件。 | `timeline title 互联网历史 1969 : ARPANET 诞生 1973 : 以太网发明 1983 : DNS 创建` |
| 思维导图 (Mindmap) | 可视化想法及其关系 | 以 `mindmap` 开始,用缩进定义层次结构。 | `mindmap 根节点 子节点 1 孙节点 1 孙节点 2 子节点 2` |
""";
}

View File

@@ -5,8 +5,27 @@ spring:
name: ai-analysis
ai:
openai:
base-url: https://api.deepseek.com
api-key: "sk-3e1935e3ffb64ab096384bca071e2841"
base-url: https://api.siliconflow.cn
api-key: "sk-xrguybusoqndpqvgzgvllddzgjamksuecyqdaygdwnrnqfwo"
chat:
options:
model: "deepseek-chat"
model: "Qwen/Qwen2.5-32B-Instruct"
ollama:
chat:
model: "qwen2.5:7b"
options:
max-tokens: 8092
zhipuai:
api-key: d1e97306540d12bb2f834be961fcacb1.SNBShlCxWYJCx0qZ
chat:
options:
model: "glm-4-flash"
# openai:
# base-url: http://173.0.59.240:8093
# api-key: test
mvc:
async:
request-timeout: -1
logging:
level:
org.springframework.ai: trace

View File

@@ -1,5 +1,5 @@
### Chat
POST http://localhost:7891/chat/stream
Content-Type: text/plain
Content-Type: application/x-www-form-urlencoded
你好
prompt=tb_app_collect_table_info中src_schema字段各有多少记录&use_database=true&database_url=jdbc:mysql://localhost:3307/main&database_username=test&database_password=test&database_name=main