diff --git a/index.html b/index.html index 8388c4b..6f5cedf 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,15 @@ - + - + + - Vite + Vue + TCM Agent 控制台 +
diff --git a/package.json b/package.json index 3c82eaa..27491a2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "spa-view", + "name": "nl-tcm-agent-admin", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -9,23 +9,18 @@ "preview": "vite preview" }, "dependencies": { - "tailwindcss": "^4.1.4", - "vue": "^3.5.13", - "@ant-design-vue/pro-layout": "^3.2.5", "@ant-design/icons-vue": "^7.0.1", - "@fortawesome/fontawesome-free": "^6.7.2", "@tailwindcss/vite": "^4.1.4", - "@vueuse/head": "^2.0.0", "ant-design-vue": "^4.1.1", "axios": "^1.6.2", - "lucide-vue-next": "^0.507.0", + "echarts": "^5.5.0", "pinia": "^2.1.7", - "swiper": "^11.0.5", + "tailwindcss": "^4.1.4", + "vue": "^3.5.13", "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.2", - "sass": "^1.71.0", "vite": "^6.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 061cee9..0a53eac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,36 +8,24 @@ importers: .: dependencies: - '@ant-design-vue/pro-layout': - specifier: ^3.2.5 - version: 3.2.5(ant-design-vue@4.2.6(vue@3.5.13))(vue@3.5.13) '@ant-design/icons-vue': specifier: ^7.0.1 version: 7.0.1(vue@3.5.13) '@tailwindcss/vite': specifier: ^4.1.4 version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)) - '@vueuse/head': - specifier: ^2.0.0 - version: 2.0.0(vue@3.5.13) ant-design-vue: specifier: ^4.1.1 version: 4.2.6(vue@3.5.13) - aos: - specifier: ^2.3.4 - version: 2.3.4 axios: specifier: ^1.6.2 version: 1.9.0 - lucide-vue-next: - specifier: ^0.507.0 - version: 0.507.0(vue@3.5.13) + echarts: + specifier: ^5.5.0 + version: 5.6.0 pinia: specifier: ^2.1.7 version: 2.3.1(vue@3.5.13) - swiper: - specifier: ^11.0.5 - version: 11.2.6 tailwindcss: specifier: ^4.1.4 version: 4.1.4 @@ -51,32 +39,18 @@ importers: '@vitejs/plugin-vue': specifier: ^5.2.2 version: 5.2.3(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13) - sass: - specifier: ^1.71.0 - version: 1.88.0 vite: specifier: ^6.3.1 version: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0) packages: - '@ant-design-vue/pro-layout@3.2.5': - resolution: {integrity: sha512-M87KEnnt3h5zLlYXPST4fTGHkuwnJac6xbBGOUTjNjMraOyyO9afx++UNdeOc8gtjtasOBcp9VYGFrGUQyJxVg==} - peerDependencies: - ant-design-vue: '>=3.2.0' - vue: '>=3.2.0' - '@ant-design/colors@6.0.0': resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} '@ant-design/icons-svg@4.4.2': resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} - '@ant-design/icons-vue@6.1.0': - resolution: {integrity: sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==} - peerDependencies: - vue: '>=3.0.3' - '@ant-design/icons-vue@7.0.1': resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==} peerDependencies: @@ -565,23 +539,6 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@unhead/dom@1.11.20': - resolution: {integrity: sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==} - - '@unhead/schema@1.11.20': - resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==} - - '@unhead/shared@1.11.20': - resolution: {integrity: sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==} - - '@unhead/ssr@1.11.20': - resolution: {integrity: sha512-j6ehzmdWGAvv0TEZyLE3WBnG1ULnsbKQcLqBDh3fvKS6b3xutcVZB7mjvrVE7ckSZt6WwOtG0ED3NJDS7IjzBA==} - - '@unhead/vue@1.11.20': - resolution: {integrity: sha512-sqQaLbwqY9TvLEGeq8Fd7+F2TIuV3nZ5ihVISHjWpAM3y7DwNWRU7NmT9+yYT+2/jw1Vjwdkv5/HvDnvCLrgmg==} - peerDependencies: - vue: '>=2.7 || >=3' - '@vitejs/plugin-vue@5.2.3': resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -621,20 +578,12 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} - '@vueuse/head@2.0.0': - resolution: {integrity: sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==} - peerDependencies: - vue: '>=2.7 || >=3' - ant-design-vue@4.2.6: resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==} engines: {node: '>=12.22.0'} peerDependencies: vue: '>=3.2.0' - aos@2.3.4: - resolution: {integrity: sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==} - array-tree-filter@2.1.0: resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} @@ -659,9 +608,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - classlist-polyfill@1.2.0: - resolution: {integrity: sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -701,6 +647,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -793,9 +742,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - immutable@5.1.2: resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==} @@ -893,12 +839,6 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - - lodash.throttle@4.1.1: - resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -906,11 +846,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lucide-vue-next@0.507.0: - resolution: {integrity: sha512-n0AZRmez4xq5vu5ZOfnrhC5wKBpu5tpwDHA6Ue2+sKyJv3USAPxqIerXLbdYbkHRW1tVZ3U9GE1MACsu6X/Z7Q==} - peerDependencies: - vue: '>=3.0.1' - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -941,12 +876,6 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - omit.js@2.0.2: - resolution: {integrity: sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==} - - packrup@0.1.2: - resolution: {integrity: sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1004,10 +933,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - swiper@11.2.6: - resolution: {integrity: sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==} - engines: {node: '>= 4.7.0'} - tailwindcss@4.1.4: resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} @@ -1027,8 +952,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - unhead@1.11.20: - resolution: {integrity: sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} vite@6.3.3: resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} @@ -1103,30 +1028,17 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - zhead@2.2.4: - resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} snapshots: - '@ant-design-vue/pro-layout@3.2.5(ant-design-vue@4.2.6(vue@3.5.13))(vue@3.5.13)': - dependencies: - '@ant-design/icons-vue': 6.1.0(vue@3.5.13) - ant-design-vue: 4.2.6(vue@3.5.13) - omit.js: 2.0.2 - vue: 3.5.13 - '@ant-design/colors@6.0.0': dependencies: '@ctrl/tinycolor': 3.6.1 '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons-vue@6.1.0(vue@3.5.13)': - dependencies: - '@ant-design/colors': 6.0.0 - '@ant-design/icons-svg': 4.4.2 - vue: 3.5.13 - '@ant-design/icons-vue@7.0.1(vue@3.5.13)': dependencies: '@ant-design/colors': 6.0.0 @@ -1424,34 +1336,6 @@ snapshots: '@types/estree@1.0.7': {} - '@unhead/dom@1.11.20': - dependencies: - '@unhead/schema': 1.11.20 - '@unhead/shared': 1.11.20 - - '@unhead/schema@1.11.20': - dependencies: - hookable: 5.5.3 - zhead: 2.2.4 - - '@unhead/shared@1.11.20': - dependencies: - '@unhead/schema': 1.11.20 - packrup: 0.1.2 - - '@unhead/ssr@1.11.20': - dependencies: - '@unhead/schema': 1.11.20 - '@unhead/shared': 1.11.20 - - '@unhead/vue@1.11.20(vue@3.5.13)': - dependencies: - '@unhead/schema': 1.11.20 - '@unhead/shared': 1.11.20 - hookable: 5.5.3 - unhead: 1.11.20 - vue: 3.5.13 - '@vitejs/plugin-vue@5.2.3(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13)': dependencies: vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0) @@ -1513,14 +1397,6 @@ snapshots: '@vue/shared@3.5.13': {} - '@vueuse/head@2.0.0(vue@3.5.13)': - dependencies: - '@unhead/dom': 1.11.20 - '@unhead/schema': 1.11.20 - '@unhead/ssr': 1.11.20 - '@unhead/vue': 1.11.20(vue@3.5.13) - vue: 3.5.13 - ant-design-vue@4.2.6(vue@3.5.13): dependencies: '@ant-design/colors': 6.0.0 @@ -1547,12 +1423,6 @@ snapshots: vue-types: 3.0.2(vue@3.5.13) warning: 4.0.3 - aos@2.3.4: - dependencies: - classlist-polyfill: 1.2.0 - lodash.debounce: 4.0.8 - lodash.throttle: 4.1.1 - array-tree-filter@2.1.0: {} async-validator@4.2.5: {} @@ -1580,8 +1450,7 @@ snapshots: chokidar@4.0.3: dependencies: readdirp: 4.1.2 - - classlist-polyfill@1.2.0: {} + optional: true combined-stream@1.0.8: dependencies: @@ -1612,6 +1481,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -1719,9 +1593,8 @@ snapshots: dependencies: function-bind: 1.1.2 - hookable@5.5.3: {} - - immutable@5.1.2: {} + immutable@5.1.2: + optional: true is-extglob@2.1.1: optional: true @@ -1787,20 +1660,12 @@ snapshots: lodash-es@4.17.21: {} - lodash.debounce@4.0.8: {} - - lodash.throttle@4.1.1: {} - lodash@4.17.21: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - lucide-vue-next@0.507.0(vue@3.5.13): - dependencies: - vue: 3.5.13 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -1826,10 +1691,6 @@ snapshots: node-addon-api@7.1.1: optional: true - omit.js@2.0.2: {} - - packrup@0.1.2: {} - picocolors@1.1.1: {} picomatch@2.3.1: @@ -1853,7 +1714,8 @@ snapshots: proxy-from-env@1.1.0: {} - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true resize-observer-polyfill@1.5.1: {} @@ -1890,6 +1752,7 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.1 + optional: true scroll-into-view-if-needed@2.2.31: dependencies: @@ -1901,8 +1764,6 @@ snapshots: stylis@4.3.6: {} - swiper@11.2.6: {} - tailwindcss@4.1.4: {} tapable@2.2.1: {} @@ -1919,12 +1780,7 @@ snapshots: is-number: 7.0.0 optional: true - unhead@1.11.20: - dependencies: - '@unhead/dom': 1.11.20 - '@unhead/schema': 1.11.20 - '@unhead/shared': 1.11.20 - hookable: 5.5.3 + tslib@2.3.0: {} vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0): dependencies: @@ -1966,4 +1822,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - zhead@2.2.4: {} + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/src/App.vue b/src/App.vue index 3602e60..188fe5d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,53 @@ - - diff --git a/src/api/agent.js b/src/api/agent.js new file mode 100644 index 0000000..51e4f13 --- /dev/null +++ b/src/api/agent.js @@ -0,0 +1,21 @@ +import { get, post } from './request' + +// ---------------- 运行观测(内存环形缓冲,最近 200 条) ---------------- +export const getRuns = (params, opts) => get('/agent/runs', params, opts) // {limit, scene, status} +export const getRunDetail = (id, opts) => get(`/agent/runs/${id}`, {}, opts) +export const getStats = (opts) => get('/agent/stats', {}, opts) + +// ---------------- 实时日志 / 系统状态 / 配置 ---------------- +export const getLogs = (params, opts) => get('/agent/logs', params, opts) // {since_id, limit} +export const getSystem = (opts) => get('/agent/system', {}, opts) +export const getAgentConfig = (opts) => get('/agent/config', {}, opts) + +// ---------------- 调试工具 ---------------- +export const guardTest = (text) => post('/agent/guard-test', { text }) +export const kbTest = (data) => post('/agent/kb-test', data) // {query, top_k} +export const enhanceTest = (data) => post('/agent/enhance', data) // 全链路测试 + +// ---------------- AI 生成历史(DB 长期数据,只读) ---------------- +export const getHistory = (params) => get('/agent/history', params) +export const getHistoryDetail = (id) => get(`/agent/history/${id}`) +export const getHistoryScenes = () => get('/agent/history/scenes', {}, { silent: true }) diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..57f29ba --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,6 @@ +import { get, post } from './request' + +// 登录鉴权相关接口 +export const loginApi = (data) => post('/auth/login', data) // {username, password, captcha} +export const refreshApi = () => post('/auth/refresh') // 带旧 token 换新 token +export const profileApi = () => get('/auth/profile', {}, { silent: true }) // 会话有效性校验 diff --git a/src/api/kb.js b/src/api/kb.js new file mode 100644 index 0000000..48ce849 --- /dev/null +++ b/src/api/kb.js @@ -0,0 +1,35 @@ +import { get, post, put, del, upload } from './request' + +// ---------------- 知识库:库管理 ---------------- +export const getLibraries = (opts) => get('/kb/admin/libraries', {}, opts) +export const getLibrary = (id) => get(`/kb/admin/libraries/${id}`) +export const createLibrary = (data) => post('/kb/admin/libraries', data) +export const deleteLibrary = (id) => del(`/kb/admin/libraries/${id}`) + +// ---------------- 知识库:文档管理 ---------------- +export const getDocs = (libraryId) => get(`/kb/admin/libraries/${libraryId}/docs`) +export const getDoc = (id) => get(`/kb/admin/docs/${id}`) +export const deleteDoc = (id) => del(`/kb/admin/docs/${id}`) +// 导入:formData 需含 library_id / file,可选 title / max_len / overlap +// opts 可传 { silent: true }(批量导入逐文件报错由队列 UI 展示,不弹全局 message) +export const importDoc = (formData, opts) => upload('/kb/admin/docs/import', formData, { timeout: 300000, ...opts }) + +// ---------------- 知识库:药品抓取任务 ---------------- +export const getCrawlSources = () => get('/kb/admin/crawl/sources') +export const getCrawlTasks = (opts) => get('/kb/admin/crawl/tasks', {}, opts) +export const createCrawlTask = (data) => post('/kb/admin/crawl/tasks', data) +export const updateCrawlTask = (id, data) => put(`/kb/admin/crawl/tasks/${id}`, data) +export const deleteCrawlTask = (id) => del(`/kb/admin/crawl/tasks/${id}`) +export const runCrawlTask = (id) => post(`/kb/admin/crawl/tasks/${id}/run`) +export const getCrawlLogs = (id, limit = 20) => get(`/kb/admin/crawl/tasks/${id}/logs`, { limit }) +// 重新分段:用新参数对已导入文档重切(旧分段软删) +export const rechunkDoc = (id, data) => post(`/kb/admin/docs/${id}/rechunk`, data) + +// ---------------- 知识库:分段管理 ---------------- +export const getChunks = (docId) => get(`/kb/admin/docs/${docId}/chunks`) +export const updateChunk = (id, data) => put(`/kb/admin/chunks/${id}`, data) +// 批量启用/禁用/删除:{ids: [], action: 'enable'|'disable'|'delete'} +export const batchChunks = (data) => put('/kb/admin/chunks/batch', data) + +// ---------------- 知识库:检索 ---------------- +export const kbSearch = (data) => post('/kb/admin/search', data) // {library_id, query, top_k} diff --git a/src/api/models.js b/src/api/models.js new file mode 100644 index 0000000..6fe1c1e --- /dev/null +++ b/src/api/models.js @@ -0,0 +1,8 @@ +import { get, post } from './request' + +// 模型管理接口 +export const getModelRoutes = (opts) => get('/models/routes', {}, opts) // 场景路由 + fallback 链 +export const registerModelRoute = (data) => post('/models/route', data) // {scene, provider} 热更新 +export const modelTest = (data) => post('/models/test', data) // {provider?, message?} +export const invalidateModelCache = () => post('/models/invalidate-cache') // 失效 LLM 配置缓存 +export const getActiveConfig = (opts) => get('/models/active-config', {}, opts) // 生效配置(脱敏) diff --git a/src/api/request.js b/src/api/request.js index 193d05f..efbab27 100644 --- a/src/api/request.js +++ b/src/api/request.js @@ -1,75 +1,99 @@ -import axios from 'axios'; -import {message as msg} from "ant-design-vue"; +import axios from 'axios' +import { message as msg } from 'ant-design-vue' + +// ======================================================================== +// HTTP 客户端 —— 适配 Go 端约定 +// ======================================================================== +// Go 端所有接口 HTTP 状态基本是 200,业务状态在 body 的 code 字段: +// { code: 200, data: ... } 成功 +// { code: 4xx/5xx, message: '' } 业务失败 +// 个别接口把数据放顶层(如 /models/routes 的 routes 字段), +// 所以这里统一返回完整 body,由调用方取 res.data / res.routes。 +// +// 鉴权:登录后 token 存 localStorage,每个请求带 Bearer JWT; +// 401 统一清会话跳登录页(带 redirect 回跳)。 +// ======================================================================== + +export const TOKEN_KEY = 'agent_admin_token' -// 创建 Axios 实例 const service = axios.create({ - baseURL: '/api/', // 这里可以设置你的 API 基础地址 - timeout: 5000 // 请求超时时间 -}); + baseURL: '/api/v1', + // enhance 全链路测试可能跑 2 分钟,超时放宽 + timeout: 150000 +}) -// 请求拦截器 -service.interceptors.request.use( - config => { - // 从本地存储中获取 token - const token = localStorage.getItem('token'); - if (token) { - // 设置请求头中的 Authorization - config.headers['Authorization'] = `Bearer ${token}`; - } - return config; - }, - error => { - console.log(error); // 打印错误信息 - return Promise.reject(error); +// 请求拦截:带上登录 token +service.interceptors.request.use(config => { + const token = localStorage.getItem(TOKEN_KEY) + if (token) { + config.headers['Authorization'] = `Bearer ${token}` } -); + return config +}) -// 响应拦截器 +// 跳登录页(保留当前路径用于登录后回跳) +function gotoLogin() { + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem('agent_admin_user') + const base = import.meta.env.BASE_URL || '/' + // 当前已在登录页就不再跳(防循环) + if (window.location.pathname.startsWith(base + 'login')) return + const redirect = encodeURIComponent(window.location.pathname + window.location.search) + window.location.href = `${base}login?redirect=${redirect}` +} + +// 响应拦截:统一处理业务 code service.interceptors.response.use( response => { - const {code, result, message} = response.data; - if (code === 0) { - return result; - } else { - if (code === 401) { - msg.error(message).then(r => { - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/' - msg.destroy() - }) - return; + const body = response.data + // 非 JSON(如导出下载)原样返回 + if (typeof body !== 'object' || body === null) return body + const { code, message } = body + if (code === undefined || code === 200) return body + if (code === 401) { + // 登录接口自己的 401(密码错误)不跳转,由登录页展示 + if ((response.config.url || '').includes('/auth/login')) { + return Promise.reject(new Error(message || '登录失败')) } - msg.error(message).then(r => { - msg.destroy() - }) + gotoLogin() + return Promise.reject(new Error(message || '会话失效')) } + // silent 标记的请求(轮询类)不弹全局报错 + if (!response.config.silentError) { + msg.error(message || `请求失败(${code})`) + } + return Promise.reject(new Error(message || `请求失败(${code})`)) }, error => { - console.log('err' + error); // 打印错误信息 - return Promise.reject(error); - } -); - -// 封装 get 请求 -const get = (url, params = {}) => { - return service.get(url, { params }); -}; - -// 封装 post 请求 -const post = (url, data = {}) => { - return service.post(url, data); -}; - -// 封装上传文件请求 -const upload = (url, file) => { - const formData = new FormData(); - formData.append('file', file); - return service.post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data' + // HTTP 层错误(网络断开 / 503 限流 / 超时) + if (error.response && error.response.status === 401) { + gotoLogin() + } else if (error.response && error.response.data && error.response.data.message) { + // Go 端 503 并发限流等场景带 message + if (!error.config?.silentError) msg.error(error.response.data.message) + } else if (!error.config?.silentError) { + msg.error(error.message?.includes('timeout') ? '请求超时' : '网络异常,服务可能不可达') } - }); -}; + return Promise.reject(error) + } +) -export { get, post, upload }; \ No newline at end of file +// opts 支持 { silent: true }(轮询类不弹全局报错)与其他 axios 配置 +function buildCfg(opts = {}) { + const { silent, ...rest } = opts + return { silentError: !!silent, ...rest } +} + +export const get = (url, params = {}, opts = {}) => service.get(url, { params, ...buildCfg(opts) }) +export const post = (url, data = {}, opts = {}) => service.post(url, data, buildCfg(opts)) +export const put = (url, data = {}, opts = {}) => service.put(url, data, buildCfg(opts)) +export const del = (url, opts = {}) => service.delete(url, buildCfg(opts)) + +// 上传(FormData 直传,附加字段由调用方拼进 formData) +export const upload = (url, formData, opts = {}) => + service.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, ...buildCfg(opts) }) + +// 无拦截器的裸探活(离线检测用:不能被 401 跳转/报错弹窗干扰) +export const probeHealth = () => axios.get('/health', { timeout: 5000 }) + +export default service diff --git a/src/api/request/upload.js b/src/api/request/upload.js deleted file mode 100644 index f7d7d88..0000000 --- a/src/api/request/upload.js +++ /dev/null @@ -1,10 +0,0 @@ -import {upload} from '../request.js' - -const prefix = 'upload' - -export function uploadImageApi(credentials) { - return upload(`${prefix}/image`, credentials) -} -export function uploadVideoApi(credentials) { - return upload(`${prefix}/video`, credentials) -} \ No newline at end of file diff --git a/src/api/request/user.js b/src/api/request/user.js deleted file mode 100644 index 54a22dc..0000000 --- a/src/api/request/user.js +++ /dev/null @@ -1,92 +0,0 @@ -import {get, post} from '../request.js' - -const prefix = 'user' - -/** - * 登录 - * @param credentials - * @returns {Promise>} - */ -export function loginApi(credentials) { - return post('login', credentials) -} -/** - * 注册 - * @param credentials - * @returns {Promise>} - */ -export function registerApi(credentials) { - return post('register', credentials) -} - -/** - * 获取用户列表 - * @param credentials - * @returns {Promise>} - */ -export function userListApi(credentials) { - return get(`${prefix}/list`, credentials) -} - -/** - * 获取当前登录用户信息 - * @param credentials - * @returns {Promise>} - */ -export function userMyInfoApi(credentials) { - return get(`${prefix}/my-info`, credentials) -} - -/** - * 获取用户统计信息 - * @param credentials - * @returns {Promise>} - */ -export function userStatsApi(credentials) { - return get(`${prefix}/user-stats`, credentials) -} - -/** - * 创建用户 - * @param credentials - * @returns {Promise>} - */ -export function createUserApi(credentials) { - return post(`${prefix}/create`, credentials) -} - -/** - * 更新用户 - * @param credentials - * @returns {Promise>} - */ -export function updateUserApi(credentials) { - return post(`${prefix}/update`, credentials) -} - -/** - * 删除用户 - * @param credentials - * @returns {Promise>} - */ -export function deleteUserApi(credentials) { - return post(`${prefix}/delete`, credentials) -} - -/** - * 重置密码 - * @param credentials - * @returns {Promise>} - */ -export function resetPasswordApi(credentials) { - return post(`${prefix}/reset-password`, credentials) -} - -/** - * 重置密码 - * @param credentials - * @returns {Promise>} - */ -export function changePasswordApi(credentials) { - return post(`${prefix}/change-password`, credentials) -} \ No newline at end of file diff --git a/src/components/CommandPalette.vue b/src/components/CommandPalette.vue new file mode 100644 index 0000000..0ba793b --- /dev/null +++ b/src/components/CommandPalette.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/components/EChart.vue b/src/components/EChart.vue new file mode 100644 index 0000000..dfd4eaa --- /dev/null +++ b/src/components/EChart.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/EmptyHint.vue b/src/components/EmptyHint.vue new file mode 100644 index 0000000..f44a523 --- /dev/null +++ b/src/components/EmptyHint.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/GlassCard.vue b/src/components/GlassCard.vue new file mode 100644 index 0000000..636ac92 --- /dev/null +++ b/src/components/GlassCard.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/HelpTip.vue b/src/components/HelpTip.vue new file mode 100644 index 0000000..585b240 --- /dev/null +++ b/src/components/HelpTip.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/JsonViewer.vue b/src/components/JsonViewer.vue new file mode 100644 index 0000000..797de9e --- /dev/null +++ b/src/components/JsonViewer.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/KvGrid.vue b/src/components/KvGrid.vue new file mode 100644 index 0000000..c7737d9 --- /dev/null +++ b/src/components/KvGrid.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/PageHeader.vue b/src/components/PageHeader.vue new file mode 100644 index 0000000..4fd6ebc --- /dev/null +++ b/src/components/PageHeader.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/RunDetailDrawer.vue b/src/components/RunDetailDrawer.vue new file mode 100644 index 0000000..07946c5 --- /dev/null +++ b/src/components/RunDetailDrawer.vue @@ -0,0 +1,123 @@ + + + diff --git a/src/components/SceneTag.vue b/src/components/SceneTag.vue new file mode 100644 index 0000000..c36a5e3 --- /dev/null +++ b/src/components/SceneTag.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/StatCard.vue b/src/components/StatCard.vue new file mode 100644 index 0000000..f0214ec --- /dev/null +++ b/src/components/StatCard.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/StatusTag.vue b/src/components/StatusTag.vue new file mode 100644 index 0000000..b178cbb --- /dev/null +++ b/src/components/StatusTag.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/StepTimeline.vue b/src/components/StepTimeline.vue new file mode 100644 index 0000000..a4cb6bc --- /dev/null +++ b/src/components/StepTimeline.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/composables/usePolling.js b/src/composables/usePolling.js new file mode 100644 index 0000000..67e0fbd --- /dev/null +++ b/src/composables/usePolling.js @@ -0,0 +1,48 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +// 统一轮询 composable +// +// 特性: +// - 组件卸载自动清理(防泄漏) +// - document 隐藏时自动暂停(后台标签页不空转、不浪费服务端配额) +// - pause/resume 手动控制(AutoRefresh 开关用) +// +// 用法:const { pause, resume } = usePolling(fetchData, 5000) +export function usePolling(fn, intervalMs, { immediate = true, enabled = true } = {}) { + let timer = null + const running = ref(false) + let userPaused = !enabled + + async function tick() { + try { await fn() } catch { /* 轮询失败静默,由调用方自行处理错误态 */ } + } + + function start() { + if (timer || userPaused) return + running.value = true + timer = setInterval(() => { + if (document.visibilityState === 'hidden') return + tick() + }, intervalMs) + } + + function stop() { + if (timer) { clearInterval(timer); timer = null } + running.value = false + } + + function pause() { userPaused = true; stop() } + function resume({ runNow = true } = {}) { + userPaused = false + if (runNow) tick() + start() + } + + onMounted(() => { + if (immediate && !userPaused) tick() + start() + }) + onUnmounted(stop) + + return { pause, resume, running, tick } +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..79139ba --- /dev/null +++ b/src/layouts/AdminLayout.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/src/layouts/Header.vue b/src/layouts/Header.vue deleted file mode 100644 index 5dfaf00..0000000 --- a/src/layouts/Header.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/src/main.js b/src/main.js index 4f9904b..8c1ca36 100644 --- a/src/main.js +++ b/src/main.js @@ -1,16 +1,13 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import Antd from 'ant-design-vue' +import 'ant-design-vue/dist/reset.css' import './style.css' import App from './App.vue' -import '@fortawesome/fontawesome-free/css/all.css' // 引入所有样式 -// 注册路由 import router from './router' +// TCM Agent 管理前端入口:Pinia → Router → Ant Design Vue const app = createApp(App) - - -// 注册Ant Design组件 app.use(createPinia()) app.use(router) app.use(Antd) diff --git a/src/router/index.js b/src/router/index.js index 796771d..8f2cdf1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,38 +1,62 @@ import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' -import MainLayout from '@/layouts/Header.vue' +import AdminLayout from '@/layouts/AdminLayout.vue' -// 在路由配置中添加个人中心路由 +// 路由表:/login 独立,其余全部挂在 AdminLayout 下(登录守卫统一控制) +// meta.title 同时用于菜单、面包屑与命令面板搜索 const routes = [ - { - path: '/', - component: MainLayout, - children: [ - { path: '', name: 'Home', component: () => import('@/views/index/index.vue') }, - ] - }, + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/index.vue'), + meta: { title: '登录' } + }, + { + path: '/', + component: AdminLayout, + redirect: '/dashboard', + meta: { requiresAuth: true }, + children: [ + { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '仪表盘', group: '监控' } }, + { path: 'runs', name: 'Runs', component: () => import('@/views/runs/index.vue'), meta: { title: '运行记录', group: '监控' } }, + { path: 'history', name: 'History', component: () => import('@/views/history/index.vue'), meta: { title: '历史记录', group: '监控' } }, + { path: 'stats', name: 'Stats', component: () => import('@/views/stats/index.vue'), meta: { title: '统计分析', group: '监控' } }, + { path: 'logs', name: 'Logs', component: () => import('@/views/logs/index.vue'), meta: { title: '实时日志', group: '监控' } }, + { path: 'system', name: 'System', component: () => import('@/views/system/index.vue'), meta: { title: '系统状态', group: '监控' } }, + { path: 'debug', name: 'Debug', component: () => import('@/views/debug/index.vue'), meta: { title: '调试工具', group: 'AI 管理' } }, + { path: 'models', name: 'Models', component: () => import('@/views/models/index.vue'), meta: { title: '模型管理', group: 'AI 管理' } }, + { path: 'config', name: 'Config', component: () => import('@/views/config/index.vue'), meta: { title: '配置总览', group: 'AI 管理' } }, + { path: 'kb', name: 'Kb', component: () => import('@/views/kb/index.vue'), meta: { title: '知识库管理', group: '知识库' } }, + { path: 'kb-search', name: 'KbSearch', component: () => import('@/views/kb-search/index.vue'), meta: { title: '知识库检索', group: '知识库' } }, + { path: 'kb-crawl', name: 'KbCrawl', component: () => import('@/views/kb-crawl/index.vue'), meta: { title: '药品抓取', group: '知识库' } } + ] + }, + { path: '/:pathMatch(.*)*', redirect: '/dashboard' } ] const router = createRouter({ - history: createWebHistory(), - routes, - scrollBehavior(to, from, savedPosition) { - return savedPosition || { top: 0 } - } + // base 是 /admin/(vite base 注入),history 模式由 Go 端 NoRoute 回退支撑刷新 + history: createWebHistory(import.meta.env.BASE_URL), + routes, + scrollBehavior(to, from, savedPosition) { + return savedPosition || { top: 0 } + } }) -router.beforeEach((to, from) => { - const userStore = useUserStore() - if (to.meta.requiresAuth && !userStore.user) { - return { name: 'Login', query: { redirect: to.fullPath } } - } +// 登录守卫:未登录访问受保护页 → 登录页(带 redirect 回跳); +// 已登录访问登录页 → 直接进仪表盘 +router.beforeEach((to) => { + const userStore = useUserStore() + if (to.meta.requiresAuth && !userStore.isAuthenticated) { + return { name: 'Login', query: { redirect: to.fullPath } } + } + if (to.name === 'Login' && userStore.isAuthenticated) { + return { path: '/dashboard' } + } +}) + +router.afterEach((to) => { + document.title = to.meta.title ? `${to.meta.title} · TCM Agent 控制台` : 'TCM Agent 控制台' }) export default router - -router.beforeEach((to, from) => { - const userStore = useUserStore() - if (to.meta.requiresAuth && !userStore.user) { - return { name: 'Login', query: { redirect: to.fullPath } } - } -}) diff --git a/src/stores/app.js b/src/stores/app.js new file mode 100644 index 0000000..9b3bf0d --- /dev/null +++ b/src/stores/app.js @@ -0,0 +1,91 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// ======================================================================== +// 全局应用 store —— 页面互通的三大底座之一 +// ======================================================================== +// - 全局运行详情抽屉:任何页面 openRun(id) 即可打开,不用先跳页 +// - 命令面板(Ctrl+K)开关 +// - 服务离线状态(health 轮询驱动) +// - 失败告警(铃铛红点 + 浏览器通知开关) +// - 重放载荷:运行详情「重放」→ 调试台回填的桥梁 +// - 新手引导 Tour 开关 +// ======================================================================== + +const THEME_KEY = 'agent_admin_theme' +const NOTIFY_KEY = 'agent_admin_notify' + +export const useAppStore = defineStore('app', () => { + // ---------- 主题(暗色暖调优先) ---------- + const theme = ref(localStorage.getItem(THEME_KEY) || 'dark') + applyTheme(theme.value) + function applyTheme(t) { + document.documentElement.dataset.theme = t + } + function toggleTheme() { + theme.value = theme.value === 'dark' ? 'light' : 'dark' + localStorage.setItem(THEME_KEY, theme.value) + applyTheme(theme.value) + } + + // ---------- 全局运行详情抽屉 ---------- + const runDrawerOpen = ref(false) + const runDrawerId = ref(0) + function openRun(id) { + runDrawerId.value = Number(id) + runDrawerOpen.value = true + } + function closeRun() { + runDrawerOpen.value = false + } + + // ---------- 命令面板 ---------- + const paletteOpen = ref(false) + + // ---------- 服务离线检测(AdminLayout 轮询 /health 驱动) ---------- + const offline = ref(false) + const offlineSince = ref(0) + + // ---------- 失败告警 ---------- + const alarmCount = ref(0) // 铃铛红点数(自上次查看以来的新失败) + const lastFailedTotal = ref(-1) // 上一次轮询看到的失败总数(-1=未初始化) + const notifyEnabled = ref(localStorage.getItem(NOTIFY_KEY) !== '0') + function toggleNotify() { + notifyEnabled.value = !notifyEnabled.value + localStorage.setItem(NOTIFY_KEY, notifyEnabled.value ? '1' : '0') + } + function clearAlarm() { alarmCount.value = 0 } + + // ---------- 重放载荷(运行详情 → Enhance 调试台) ---------- + const replayPayload = ref(null) // { fromRunId, scene, context, kb_enabled, top_k, messages } + function setReplay(payload) { replayPayload.value = payload } + function consumeReplay() { + const p = replayPayload.value + replayPayload.value = null + return p + } + + // ---------- KB 检索 → 调试台注入 ---------- + const injectContext = ref(null) // { query, content } + function setInject(payload) { injectContext.value = payload } + function consumeInject() { + const p = injectContext.value + injectContext.value = null + return p + } + + // ---------- 新手引导 ---------- + const tourOpen = ref(false) + function startTour() { tourOpen.value = true } + + return { + theme, toggleTheme, + runDrawerOpen, runDrawerId, openRun, closeRun, + paletteOpen, + offline, offlineSince, + alarmCount, lastFailedTotal, notifyEnabled, toggleNotify, clearAlarm, + replayPayload, setReplay, consumeReplay, + injectContext, setInject, consumeInject, + tourOpen, startTour + } +}) diff --git a/src/stores/user.js b/src/stores/user.js index 5987aad..0e67ab6 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -1,87 +1,72 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import {loginApi, registerApi} from "@/api/request/user.js"; -import {message, notification} from "ant-design-vue"; +import { notification } from 'ant-design-vue' +import { loginApi, refreshApi, profileApi } from '@/api/auth' +import { TOKEN_KEY } from '@/api/request' +const USER_KEY = 'agent_admin_user' +const EXPIRE_KEY = 'agent_admin_expire' + +// 用户会话 store:登录 / 登出 / 会话校验 / 静默续签 export const useUserStore = defineStore('user', () => { - const user = ref(JSON.parse(localStorage.getItem('user') || 'null')) - const modal = ref(localStorage.getItem('authModal') || 'false') + const token = ref(localStorage.getItem(TOKEN_KEY) || '') + const user = ref(safeParse(localStorage.getItem(USER_KEY))) + const expireAt = ref(Number(localStorage.getItem(EXPIRE_KEY) || 0)) - // 添加初始化状态校验 - const initUser = () => { - const userData = localStorage.getItem('user') - if (!userData) return null - try { - return JSON.parse(userData) - } catch { - localStorage.removeItem('user') - localStorage.removeItem('token') - return null + const isAuthenticated = computed(() => !!token.value) + + function safeParse(s) { + try { return s ? JSON.parse(s) : null } catch { return null } } - } - - const isAuthenticated = computed(() => !!user.value?.nick_name) - - function login(credentials, functionName) { - console.log(functionName, 'sssssssssssss') - loginApi({ - account: credentials.username, - password: credentials.password, - remember: credentials.remember, - }).then((res) => { - const userData = { - nick_name: res.nick_name, - role_name: res.role_name, - role_value: res.role_value, - } - user.value = userData; - localStorage.setItem('user', JSON.stringify(userData)) - localStorage.setItem('token', res.token) - notification.success({ - message: '登录成功', - description: `欢迎回来${res.nick_name}`, - }) - functionName() - }) - } - function register(credentials, functionName) { - registerApi({ - account: credentials.username, - password: credentials.password, - email: credentials.email, - code: credentials.code, - }).then((res) => { - const userData = { - nick_name: res.nick_name, - role_name: res.role_name, - role_value: res.role_value, - } - user.value = userData; - localStorage.setItem('user', JSON.stringify(userData)) - localStorage.setItem('token', res.token) - message.success('注册成功') - functionName() - }) - } - function logout() { - user.value = null - localStorage.removeItem('user') - localStorage.removeItem('token') - } + // 登录:成功后落 localStorage 并返回,失败抛错由登录页展示(含 shake 动效) + async function login({ username, password, captcha }) { + const res = await loginApi({ username, password, captcha }) + const d = res.data + token.value = d.token + user.value = { nick_name: d.nick_name, role_name: d.role_name, role: d.role } + expireAt.value = d.expire_at || 0 + localStorage.setItem(TOKEN_KEY, d.token) + localStorage.setItem(USER_KEY, JSON.stringify(user.value)) + localStorage.setItem(EXPIRE_KEY, String(expireAt.value)) + notification.success({ message: '登录成功', description: `欢迎回来,${d.nick_name}` }) + return d + } - function setModal(value) { - modal.value = modal.value === 'false' ? 'true' : 'false'; - localStorage.setItem('authModal', value) - } + // 会话校验:启动时调 profile,token 失效会走 401 全局跳转 + async function checkSession() { + if (!token.value) return false + try { + const res = await profileApi() + user.value = { ...user.value, ...res.data } + return true + } catch { + return false + } + } - return { - modal, - user, - isAuthenticated, - setModal, - login, - register, - logout - } -}) \ No newline at end of file + // 静默续签:剩余有效期 < 24h 时换新 token(AdminLayout 定时驱动) + async function maybeRefresh() { + if (!token.value || !expireAt.value) return + const remain = expireAt.value - Math.floor(Date.now() / 1000) + if (remain > 24 * 3600 || remain <= 0) return + try { + const res = await refreshApi() + token.value = res.data.token + expireAt.value = res.data.expire_at || 0 + localStorage.setItem(TOKEN_KEY, token.value) + localStorage.setItem(EXPIRE_KEY, String(expireAt.value)) + } catch { /* 续签失败不打扰用户,到期自然走 401 */ } + } + + function logout() { + token.value = '' + user.value = null + expireAt.value = 0 + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(USER_KEY) + localStorage.removeItem(EXPIRE_KEY) + } + + return { token, user, expireAt, isAuthenticated, login, logout, checkSession, maybeRefresh } +}) diff --git a/src/style.css b/src/style.css index a461c50..b85b9ab 100644 --- a/src/style.css +++ b/src/style.css @@ -1 +1,179 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +/* ======================================================================== + TCM Agent 控制台 —— 主题变量体系(毛玻璃 + 暖色 + 暗色优先) + ======================================================================== + 设计约定: + - 暗色暖调是第一公民:暖黑底渐变,主色琥珀金,中性色全部带暖偏 + - 亮色主题作为次要选项(宝蓝主色,与旧面板同源) + - 所有卡片/抽屉/模态统一玻璃表面(.glass),层级靠透明度与边线区分 + - 状态色语义全局固定:成功绿 / 失败红 / 运行蓝 / 警告琥珀 + ======================================================================== */ + +:root[data-theme='dark'] { + --bg-base: #16100a; + --bg-grad-1: #1c1409; + --bg-grad-2: #22150b; + --glass-bg: rgba(43, 32, 19, 0.55); + --glass-bg-strong: rgba(50, 38, 22, 0.82); + --glass-border: rgba(245, 158, 11, 0.16); + --glass-hover: rgba(245, 158, 11, 0.07); + --text-1: #f3e9d6; + --text-2: #bfae8d; + --text-3: #8d7c60; + --primary: #f59e0b; + --primary-strong: #fbbf24; + --accent: #fb923c; + --ok: #22c55e; + --err: #ef4444; + --run: #3b82f6; + --warn: #f59e0b; + --shadow-glow: 0 0 24px rgba(245, 158, 11, 0.08); +} + +:root[data-theme='light'] { + --bg-base: #f2f4fb; + --bg-grad-1: #eef1fa; + --bg-grad-2: #e8ecf9; + --glass-bg: rgba(255, 255, 255, 0.72); + --glass-bg-strong: rgba(255, 255, 255, 0.92); + --glass-border: rgba(67, 97, 238, 0.14); + --glass-hover: rgba(67, 97, 238, 0.05); + --text-1: #1e2433; + --text-2: #5a6478; + --text-3: #98a1b3; + --primary: #4361ee; + --primary-strong: #3651d4; + --accent: #7c3aed; + --ok: #16a34a; + --err: #dc2626; + --run: #2563eb; + --warn: #d97706; + --shadow-glow: 0 6px 24px rgba(67, 97, 238, 0.08); +} + +/* 等宽字体栈:日志 / JSON / token 数字 / 耗时 */ +:root { + --mono: ui-monospace, 'JetBrains Mono', 'Cascadia Mono', Consolas, monospace; +} + +html, body, #app { height: 100%; } + +body { + margin: 0; + color: var(--text-1); + background-color: var(--bg-base); + /* 暖色氛围光 + 细网格纹理(纯 CSS,无图片依赖) */ + background-image: + radial-gradient(1100px 500px at 85% -10%, rgba(245, 158, 11, 0.10), transparent 60%), + radial-gradient(900px 460px at -10% 100%, rgba(251, 146, 60, 0.07), transparent 55%), + linear-gradient(160deg, var(--bg-grad-1), var(--bg-base) 55%, var(--bg-grad-2)), + repeating-linear-gradient(0deg, transparent 0 39px, rgba(245, 158, 11, 0.025) 39px 40px), + repeating-linear-gradient(90deg, transparent 0 39px, rgba(245, 158, 11, 0.025) 39px 40px); + background-attachment: fixed; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + transition: background-color .25s ease; +} + +:root[data-theme='light'] body { + background-image: + radial-gradient(1100px 500px at 85% -10%, rgba(67, 97, 238, 0.08), transparent 60%), + linear-gradient(160deg, var(--bg-grad-1), var(--bg-base) 55%, var(--bg-grad-2)); +} + +/* ---------------- 玻璃表面 ---------------- */ +.glass { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 12px; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: var(--shadow-glow); +} +.glass-strong { + background: var(--glass-bg-strong); + border: 1px solid var(--glass-border); + border-radius: 12px; + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} +.glass-hover:hover { + background: var(--glass-hover); + border-color: rgba(245, 158, 11, 0.3); + cursor: pointer; +} + +/* ---------------- 通用工具类 ---------------- */ +.mono { font-family: var(--mono); } +.text-1 { color: var(--text-1); } +.text-2 { color: var(--text-2); } +.text-3 { color: var(--text-3); } + +/* 服务健康点微光 */ +.health-dot { + width: 10px; height: 10px; border-radius: 50%; + display: inline-block; + background: var(--ok); + box-shadow: 0 0 8px var(--ok); + animation: pulse-glow 2.4s ease-in-out infinite; +} +.health-dot.down { + background: var(--err); + box-shadow: 0 0 8px var(--err); + animation: none; +} +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 4px var(--ok); } + 50% { box-shadow: 0 0 12px var(--ok); } +} + +/* 登录失败晃动 */ +@keyframes shake-x { + 0%, 100% { transform: translateX(0); } + 20%, 60% { transform: translateX(-8px); } + 40%, 80% { transform: translateX(8px); } +} +.shake { animation: shake-x .45s ease; } + +/* 尊重系统减弱动效偏好 */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } +} + +/* ---------------- antd 组件的玻璃化微调 ---------------- */ +/* 侧边栏 / 顶栏由 AdminLayout 自己的 class 控制,这里只处理弹层类 */ +.ant-modal .ant-modal-content, +.ant-drawer .ant-drawer-content { + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} +/* 表格行 hover 暖化 */ +:root[data-theme='dark'] .ant-table-tbody > tr:hover > td { + background: rgba(245, 158, 11, 0.06) !important; +} + +/* 终端风日志区 */ +.log-terminal { + font-family: var(--mono); + font-size: 12px; + line-height: 1.75; + white-space: pre-wrap; + word-break: break-all; +} +.log-terminal .log-warn { color: var(--warn); } +.log-terminal .log-err { color: var(--err); } +.log-terminal .log-ok { color: var(--ok); } + +/* 滚动条(暗色下默认滚动条太亮) */ +:root[data-theme='dark'] ::-webkit-scrollbar { width: 8px; height: 8px; } +:root[data-theme='dark'] ::-webkit-scrollbar-thumb { background: rgba(245, 158, 11, 0.18); border-radius: 4px; } +:root[data-theme='dark'] ::-webkit-scrollbar-thumb:hover { background: rgba(245, 158, 11, 0.32); } +:root[data-theme='dark'] ::-webkit-scrollbar-track { background: transparent; } + +/* 检索关键词高亮 */ +mark.kw-hl { + background: rgba(245, 158, 11, 0.28); + color: var(--primary-strong); + padding: 0 2px; + border-radius: 3px; +} diff --git a/src/utils/format.js b/src/utils/format.js new file mode 100644 index 0000000..ee245b9 --- /dev/null +++ b/src/utils/format.js @@ -0,0 +1,135 @@ +// ======================================================================== +// 展示格式化工具 + 业务常量(场景/状态映射、token 单价表) +// ======================================================================== + +// unix 秒 → 'MM-DD HH:mm:ss'(列表用短格式,省列宽) +export function fmtTime(ts) { + if (!ts) return '-' + const d = new Date(ts * 1000) + const p = n => String(n).padStart(2, '0') + return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} + +// unix 秒 → 完整 'YYYY-MM-DD HH:mm:ss' +export function fmtTimeFull(ts) { + if (!ts) return '-' + const d = new Date(ts * 1000) + const p = n => String(n).padStart(2, '0') + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} + +// 毫秒 → 人类可读(890ms / 3.2s / 1m5s) +export function fmtMs(ms) { + if (ms == null) return '-' + if (ms < 1000) return `${ms}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s` +} + +// 秒 → 运行时长(3d2h / 5h12m / 8m) +export function fmtUptime(sec) { + if (!sec) return '-' + const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60) + if (d > 0) return `${d}d${h}h` + if (h > 0) return `${h}h${m}m` + return `${m}m${sec % 60}s` +} + +// 耗时着色(列表里一眼看到慢请求) +export function msTone(ms) { + if (ms == null) return 'var(--text-2)' + if (ms < 5000) return 'var(--ok)' + if (ms < 20000) return 'var(--warn)' + return 'var(--err)' +} + +// ---------------- 场景 ---------------- +export const SCENE_LABELS = { + medical_record: '病历生成', + prescription: '处方生成', + prescription_validate: '处方校验', + knowledge_qa: '知识问答', + 'emr-generator': '病历(Go)', + 'knowledge-qa': '问答(Go)' +} +export const SCENE_OPTIONS = [ + { label: '病历生成', value: 'medical_record' }, + { label: '处方生成', value: 'prescription' }, + { label: '处方校验', value: 'prescription_validate' }, + { label: '知识问答', value: 'knowledge_qa' } +] +export function sceneLabel(s) { return SCENE_LABELS[s] || s || '-' } + +// ---------------- 运行状态(内存 RunLog:1成功 2失败 3守卫拦截) ---------------- +export const RUN_STATUS = { + 1: { label: '成功', color: 'success' }, + 2: { label: '失败', color: 'error' }, + 3: { label: '拦截', color: 'warning' } +} +// 生成历史状态(DB xk_ai_generation:0进行中 1成功 2失败) +export const GEN_STATUS = { + 0: { label: '进行中', color: 'processing' }, + 1: { label: '成功', color: 'success' }, + 2: { label: '失败', color: 'error' } +} + +// ---------------- token 成本估算 ---------------- +// 单价:元 / 1K token(输入, 输出)。内部估算用,非账单口径; +// 未知 provider 用 default 兜底 +export const TOKEN_PRICES = { + spark: { in: 0.001, out: 0.001 }, // 讯飞 lite 档约价 + deepseek: { in: 0.001, out: 0.002 }, + qwen: { in: 0.0008, out: 0.002 }, + openai: { in: 0.018, out: 0.072 }, // gpt-4o 折算人民币 + ollama: { in: 0, out: 0 }, // 本地模型无 API 成本 + default: { in: 0.002, out: 0.004 } +} +export function estimateCost(provider, promptTokens = 0, completionTokens = 0) { + const p = TOKEN_PRICES[provider] || TOKEN_PRICES.default + return (promptTokens / 1000) * p.in + (completionTokens / 1000) * p.out +} +export function fmtCost(v) { + if (v == null || isNaN(v)) return '-' + if (v === 0) return '¥0' + if (v < 0.01) return `¥${v.toFixed(4)}` + return `¥${v.toFixed(2)}` +} + +// ---------------- 步骤类型 ---------------- +export const STEP_TYPE_LABELS = { + plan: '计划', + kb_retrieval: '知识检索', + llm_call: 'LLM 调用', + tool_call: '工具调用', + reflection: '反思审核', + medical_guard: '医疗守卫', + json_repair: 'JSON 修复' +} +export function stepTypeLabel(t) { return STEP_TYPE_LABELS[t] || t } + +// 下载 JSON 文件(导出用) +export function downloadJSON(obj, filename) { + const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} + +// 复制到剪贴板(带降级) +export async function copyText(text) { + try { + await navigator.clipboard.writeText(text) + return true + } catch { + const ta = document.createElement('textarea') + ta.value = text + document.body.appendChild(ta) + ta.select() + const ok = document.execCommand('copy') + document.body.removeChild(ta) + return ok + } +} diff --git a/src/views/config/index.vue b/src/views/config/index.vue new file mode 100644 index 0000000..8198e1c --- /dev/null +++ b/src/views/config/index.vue @@ -0,0 +1,128 @@ + + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..b0a7a8b --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,191 @@ + + + diff --git a/src/views/debug/index.vue b/src/views/debug/index.vue new file mode 100644 index 0000000..3672629 --- /dev/null +++ b/src/views/debug/index.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/src/views/history/index.vue b/src/views/history/index.vue new file mode 100644 index 0000000..dddd504 --- /dev/null +++ b/src/views/history/index.vue @@ -0,0 +1,180 @@ + + + diff --git a/src/views/index/index.vue b/src/views/index/index.vue deleted file mode 100644 index 38bc235..0000000 --- a/src/views/index/index.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/views/kb-crawl/index.vue b/src/views/kb-crawl/index.vue new file mode 100644 index 0000000..ad6b6fa --- /dev/null +++ b/src/views/kb-crawl/index.vue @@ -0,0 +1,308 @@ + + + diff --git a/src/views/kb-search/index.vue b/src/views/kb-search/index.vue new file mode 100644 index 0000000..7255bb5 --- /dev/null +++ b/src/views/kb-search/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/views/kb/components/ChunkEditModal.vue b/src/views/kb/components/ChunkEditModal.vue new file mode 100644 index 0000000..d28a98d --- /dev/null +++ b/src/views/kb/components/ChunkEditModal.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/views/kb/components/ChunkListDrawer.vue b/src/views/kb/components/ChunkListDrawer.vue new file mode 100644 index 0000000..1b715af --- /dev/null +++ b/src/views/kb/components/ChunkListDrawer.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/views/kb/components/CreateLibraryModal.vue b/src/views/kb/components/CreateLibraryModal.vue new file mode 100644 index 0000000..73aca3f --- /dev/null +++ b/src/views/kb/components/CreateLibraryModal.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/kb/components/DocPreviewDrawer.vue b/src/views/kb/components/DocPreviewDrawer.vue new file mode 100644 index 0000000..aaca468 --- /dev/null +++ b/src/views/kb/components/DocPreviewDrawer.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/views/kb/components/RechunkModal.vue b/src/views/kb/components/RechunkModal.vue new file mode 100644 index 0000000..7f12fc6 --- /dev/null +++ b/src/views/kb/components/RechunkModal.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/views/kb/index.vue b/src/views/kb/index.vue new file mode 100644 index 0000000..b2d933f --- /dev/null +++ b/src/views/kb/index.vue @@ -0,0 +1,342 @@ + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..0c57dd0 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/views/models/index.vue b/src/views/models/index.vue new file mode 100644 index 0000000..743ddbb --- /dev/null +++ b/src/views/models/index.vue @@ -0,0 +1,253 @@ + + + diff --git a/src/views/runs/index.vue b/src/views/runs/index.vue new file mode 100644 index 0000000..9ab5f56 --- /dev/null +++ b/src/views/runs/index.vue @@ -0,0 +1,192 @@ + + + diff --git a/src/views/stats/index.vue b/src/views/stats/index.vue new file mode 100644 index 0000000..009f15e --- /dev/null +++ b/src/views/stats/index.vue @@ -0,0 +1,179 @@ + + + diff --git a/src/views/system/index.vue b/src/views/system/index.vue new file mode 100644 index 0000000..82ef238 --- /dev/null +++ b/src/views/system/index.vue @@ -0,0 +1,126 @@ + + + diff --git a/vite.config.js b/vite.config.js index df31b24..0369f13 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,8 +3,12 @@ import vue from '@vitejs/plugin-vue' import tailwindcss from "@tailwindcss/vite" import path from 'path' -// https://vite.dev/config/ +// TCM Agent 管理前端构建配置 +// - base 固定 /admin/:Go 端把 SPA 挂在 /admin 路径下(router.go 的 NoRoute 回退) +// - outDir 直接输出到 Go 项目的 view/admin-dist:单二进制 + 静态目录部署,无需拷贝 +// - 开发代理指向本机 Go 服务 18123(不 rewrite,Go 的路由本身就带 /api 前缀) export default defineConfig({ + base: '/admin/', plugins: [ vue(), tailwindcss() @@ -14,14 +18,21 @@ export default defineConfig({ '@': path.resolve(__dirname, './src') } }, + build: { + outDir: path.resolve(__dirname, '../nl-tcm-agent/view/admin-dist'), + emptyOutDir: true, + chunkSizeWarningLimit: 1500 + }, server: { - port: 8888, + port: 8890, proxy: { '/api': { - // target: 'http://127.0.0.1:18007/api/', - target: 'http://127.0.0.1:18001/api/', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '') + target: 'http://127.0.0.1:18123', + changeOrigin: true + }, + '/health': { + target: 'http://127.0.0.1:18123', + changeOrigin: true } } }