feat: 小鑫 AI 助手 - 前端构建打包、DuckDB 处理 Excel、设置与审批级别

This commit is contained in:
Cheney
2026-08-15 23:07:29 +08:00
commit 3b9991f9f7
23 changed files with 2614 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Mini AI Assistant 配置示例
# 复制本文件为 .env 并填入真实值(.env 已被 .gitignore 忽略,不会提交)
# 大模型接口(OpenAI 兼容)
BASE_URL=https://ark.cn-beijing.volces.com/api/coding/v3
API_KEY=your_api_key_here
MODEL=ark-code-latest
# 工作区(后端可读写的根目录,建议用绝对路径)
WORKSPACE=.
# 命令授权级别:high=每条都审核 / medium=仅危险命令审核 / low=全部不审核
CMD_APPROVE_LEVEL=medium
# 服务端口(前端构建后由后端统一托管)
PORT=3100
# 日志级别:debug / info / warn / error
LOG_LEVEL=debug
# 温度(0~1)
TEMPERATURE=0.3
# 路径白名单(允许的盘符/目录,逗号分隔;留空表示仅工作区)
WHITELIST=d:\
# 路径黑名单(优先级最高,逗号分隔;留空表示无)
BLACKLIST=
+26
View File
@@ -0,0 +1,26 @@
# 依赖
node_modules/
# 前端构建产物
dist/
# 运行日志
logs/
*.log
*.err
_be.*
# 环境变量(含密钥,勿提交;参考 .env.example)
.env
.env.local
# 系统文件
.DS_Store
Thumbs.db
# 编辑器 / IDE
.vscode/
.idea/
# bun 缓存
.bun/
+586
View File
@@ -0,0 +1,586 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "mini-ai-assistant",
"dependencies": {
"axios": "^1.7.0",
"duckdb": "^1.4.4",
"highlight.js": "^11.9.0",
"marked": "^12.0.0",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"xlsx": "^0.18.5",
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"concurrently": "^8.2.2",
"typescript": "^5.4.5",
"vite": "^5.2.0",
},
},
},
"packages": {
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="],
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
"@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
"@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
"@tootallnate/once": ["@tootallnate/once@2.0.1", "", {}, "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.41", "", { "dependencies": { "@vue/compiler-core": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.41", "@vue/compiler-dom": "3.5.41", "@vue/compiler-ssr": "3.5.41", "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/reactivity": ["@vue/reactivity@3.5.41", "", { "dependencies": { "@vue/shared": "3.5.41" } }, "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/runtime-core": "3.5.41", "@vue/shared": "3.5.41", "csstype": "^3.2.3" } }, "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.41", "", { "dependencies": { "@vue/compiler-ssr": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ=="],
"@vue/shared": ["@vue/shared@3.5.41", "", {}, "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA=="],
"abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="],
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.19.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
"cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"duckdb": ["duckdb@1.4.4", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "node-addon-api": "^7.0.0", "node-gyp": "^9.4.1" } }, "sha512-hEJJ5hyVF0VLj3dmOLpsWrQR0b3d4Ykqtygm6iUXjHJDDnSqg/USKxcb5qvNrOhFYPgktayU6dXCtgPqcmmpog=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
"frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"highlight.js": ["highlight.js@11.12.0", "", {}, "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="],
"marked": ["marked@12.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
"minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="],
"minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="],
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="],
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"pinia": ["pinia@2.3.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="],
"socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],
"ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="],
"ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="],
"unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vue": ["vue@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/compiler-sfc": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/server-renderer": "3.5.41", "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg=="],
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="],
"word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@mapbox/node-pre-gyp/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"cacache/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"node-gyp/nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="],
"node-gyp/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"@mapbox/node-pre-gyp/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"cacache/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"cacache/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"cacache/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"cacache/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"cacache/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"make-fetch-happen/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-collect/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-fetch/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-fetch/minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"node-gyp/nopt/abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"node-gyp/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"node-gyp/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"node-gyp/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"node-gyp/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"ssri/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
"cacache/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"node-gyp/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
}
}
BIN
View File
Binary file not shown.
+191
View File
@@ -0,0 +1,191 @@
# mini AI 助手软件需求规格说明书
## 1. 项目概述
本项目旨在构建一个轻量级 **桌面 AI 助手**,它允许用户通过自然语言与一个大语言模型(LLM)交互,并授权该模型执行本地文件系统操作(读写、复制、移动、删除)、Excel 数据处理、以及任意的 Shell 命令(需用户二次确认)。所有操作均在本地回环网络中进行,不依赖任何外部 Web 服务上传文件,从而规避企业或学校对网页上传的拦截策略。
前端采用 **Vue 3** 构建用户界面,后端使用 **Bun**(JavaScript 运行时)提供 HTTP 服务和 LLM 调用能力。整个项目 **纯 JavaScript / TypeScript** 实现,不依赖 Python,不依赖 C/C++ 原生库,所有第三方依赖均为开源纯 JS 模块。
---
## 2. 技术栈
| 层级 | 技术选型 | 说明 |
| :--- | :--- | :--- |
| **前端** | Vue 3 + Vite | 使用 Composition API,组件化开发 |
| | Vue Router(可选) | 若需要多页面视图 |
| | Pinia(可选) | 状态管理(对话历史、配置等) |
| | Markdown 渲染 | `markdown-parser-vue` 或 `@vueuse/head` + `marked` 或 `mermaid`(支持图表) |
| | 表格组件 | 可用 `element-plus` 的 Table 或纯 CSS 表格 |
| | 进度条 | 自定义组件或 `element-plus` 的 Progress |
| | HTTP 客户端 | `axios` 或原生 `fetch` |
| **后端** | Bun 1.0+ | JavaScript/TypeScript 运行时,内置 HTTP 服务器、文件系统 API、子进程 |
| | LLM 调用 | 使用 OpenAI 兼容 API(支持 OpenAI、Groq、本地 Ollama 等) |
| | 文件操作 | 原生 `fs` / `Bun.file` / `Bun.write` |
| | Excel 处理 | `xlsx` (SheetJS) 或 `hucre` / `modern-xlsx`(纯 JS,无原生依赖) |
| | 环境变量 | `dotenv` |
| **依赖管理** | Bun 内置包管理器 | `bun install` |
---
## 3. 用户角色
- **普通用户**:通过对话框向 AI 下达指令,审批危险操作。
- **管理员(可选)**:配置 LLM 参数(API Key、模型、温度)、工作目录白名单等。
---
## 4. 功能需求
### 4.1 对话交互
- **FR1**:前端提供一个聊天界面,支持用户输入文本,并实时显示 AI 的回复。
- **FR2**:支持 Markdown 格式渲染(包括代码块语法高亮、表格、列表、加粗、链接等)。
- **FR3**:对话历史保存在前端内存中,可清空或导出。
- **FR4**:支持流式(SSE)或非流式响应,建议优先实现流式以获得更好体验。
### 4.2 文件系统操作(AI 可调用的工具)
AI 模型将通过函数调用(Function Calling)请求执行以下操作,所有操作需在用户指定的**工作目录**(默认为项目根目录)内进行,防止越权。
| 工具名称 | 功能描述 | 参数 | 是否需审批 |
| :--- | :--- | :--- | :--- |
| `read_file` | 读取指定路径文件内容(文本) | `path` | 否 |
| `write_file` | 将内容写入指定文件(覆盖) | `path`, `content` | 否(仅写入文本) |
| `list_directory` | 列出目录下的文件和子目录 | `path`(可选) | 否 |
| `copy_file` | 复制文件或目录(递归) | `source`, `destination` | 否(但若目标覆盖需提示) |
| `move_file` | 移动/重命名文件或目录 | `source`, `destination` | 否 |
| `delete_file` | 删除文件或空目录 | `path` | **是(需二次确认)** |
| `execute_command` | 执行任意 Shell 命令 | `command` | **是(危险命令需二次确认)** |
| `read_excel` | 读取 Excel 文件,返回 JSON 数据(带行数限制) | `path`, `sheet?`, `limit?` | 否 |
| `write_excel` | 将 JSON 数据写入 Excel 文件 | `path`, `data` (JSON 数组), `sheet?` | 否 |
**审批机制**:当 AI 调用需要审批的工具时,后端不立即执行,而是向前端推送一个“审批请求”消息,包含待执行的命令/操作详情。前端弹窗让用户选择“允许”或“拒绝”,用户确认后,前端再向后端发送批准指令,后端才真正执行并将结果返回给 AI。
### 4.3 Excel 操作特殊要求
- 读取时默认最多返回前 200 行数据(可配置),但会告知总行数和列名,让 AI 据此进行数据分析(如求和、筛选)。
- 支持自动识别日期、数字格式。
- 写入时支持从 JSON 数组生成 Excel 文件,列名自动取自对象键名。
### 4.4 文件拷贝进度展示
- 当 AI 执行 `copy_file` 操作时,若拷贝大文件/目录,后端通过 WebSocket 或 SSE 向前端推送进度百分比。
- 前端在聊天消息中嵌入一个进度条组件,实时更新。
### 4.5 命令执行安全控制
- 后端在执行命令前会检查命令是否包含危险关键字(如 `rm -rf /`, `dd`, `shutdown` 等),若匹配则自动拒绝并提示用户。
- 用户可在配置中启用“严格模式”,禁止任何命令执行(此时 `execute_command` 工具直接返回“禁用”提示)。
### 4.6 多轮对话与上下文记忆
- AI 能记住对话历史(默认保留最近 20 条),并在需要时主动调用工具。
- 用户可手动清空上下文。
---
## 5. 非功能需求
### 5.1 性能
- 页面首次加载时间 ≤ 2 秒(开发环境)。
- 大文件(>100MB)读取或写入时,采用流式处理,避免内存溢出。
### 5.2 安全
- 所有文件操作路径限制在 `process.cwd()` 及其子目录下(可通过环境变量 `WORKSPACE` 修改)。
- 禁止访问系统敏感目录(如 `/etc`, `C:\Windows` 等)。
- 传输层:前后端通信使用 HTTP,仅在本地监听 `127.0.0.1`,默认不对外暴露端口。
### 5.3 可用性
- 界面风格简洁,采用深色/浅色主题(可切换)。
- 消息气泡区分用户和 AI,AI 消息支持 Markdown 渲染。
- 错误信息明确提示,并建议用户如何修改指令。
### 5.4 可扩展性
- 工具函数采用插件化设计,新增工具只需添加定义和执行函数。
- 支持更换 LLM 提供商(只需修改环境变量 `BASE_URL` 和 `API_KEY`)。
### 5.5 兼容性
- 前端运行于现代浏览器(Chrome 90+, Edge 90+, Firefox 88+)。
- 后端仅要求 Bun 1.0+,支持 Windows / macOS / Linux。
---
## 6. 界面设计概要(Vue 组件划分)
- **App.vue**:整体布局,包含头部标题和聊天容器。
- **MessageList.vue**:消息列表,遍历消息数组,根据角色渲染不同样式。
- **MessageBubble.vue**:单条消息,支持 Markdown 渲染和特殊类型(如审批请求、进度条)。
- **InputArea.vue**:输入框 + 发送按钮,支持 Enter 发送。
- **ApprovalDialog.vue**:审批弹窗,显示命令详情并提供允许/拒绝按钮。
- **ProgressBar.vue**:进度条组件,接收百分比值。
- **SettingsDrawer.vue**(可选):配置 API 密钥、模型参数等。
---
## 7. API 接口设计(前后端通信)
所有接口采用 JSON 格式。
### 7.1 发送消息(含工具调用)
- **Endpoint**: `POST /api/chat`
- **Request Body**:
```json
{
"messages": [ // 对话历史,至少包含用户最新一条
{ "role": "user", "content": "读取 data.xlsx 并告诉我销售额" }
]
}
```
- **Response**(分三种情况):
1. **直接回复**(无工具调用):
```json
{ "type": "done", "message": "销售额为 12345 元" }
```
2. **需要审批**:
```json
{
"type": "approval_required",
"tool_call_id": "call_abc",
"command": "rm -rf temp",
"assistant_message": "我想删除 temp 文件夹,请批准"
}
```
3. **工具执行结果**(已自动完成并返回最终回复):
```json
{ "type": "done", "message": "已成功写入 Excel 文件" }
```
### 7.2 批准操作
- **Endpoint**: `POST /api/approve`
- **Request Body**:
```json
{
"tool_call_id": "call_abc",
"approved": true
}
```
- **Response**: 执行结果(同上面“工具执行结果”格式)
### 7.3 进度推送(WebSocket 或 SSE)
- 若使用 WebSocket,路径 `/ws`,后端在拷贝大文件时推送 `{ "progress": 45 }`。
---
## 8. 部署与运行
- 开发环境:`bun run dev`(同时启动后端和前端,可分别用 `vite` 和 `bun` 热更新)。
- 生产环境:`bun run build` 打包前端,后端使用 `bun server.js` 运行,并静态托管前端构建产物。
- 用户仅需执行 `bun install` 安装依赖,无需全局安装任何程序。
---
## 9. 未来扩展(可选)
- 支持多 Agent 协作(多个角色)。
- 支持语音输入/输出。
- 支持插件市场,用户可自定义工具。
---
## 10. 附录:开源库推荐(符合“纯 JS,无 C 依赖”)
| 功能 | 推荐库 | 理由 |
| :--- | :--- | :--- |
| Excel 读写 | `xlsx` (SheetJS 社区版) | 最成熟,纯 JS,支持 xlsx/xls/csv |
| 或 `modern-xlsx` | 基于 Rust+WASM,但无需本地编译,通过 npm 提供 WASM 文件,也算纯 JS 使用 |
| 进度条(前端) | `element-plus` 或自定义 | UI 组件库自带的进度条 |
| Markdown 渲染 | `marked` + `highlight.js` | 轻量且高度可定制 |
| 表格组件 | `element-plus` Table 或 `ag-grid-community` | 前者轻量,后者功能丰富 |
| 文件操作(后端) | 使用 Bun 内置 API | 无需额外库 |
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini AI Assistant</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "mini-ai-assistant",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "concurrently \"vite\" \"bun run node server.ts\"",
"build": "vite build",
"start": "bun run server.ts",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.0",
"duckdb": "^1.4.4",
"highlight.js": "^11.9.0",
"marked": "^12.0.0",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.4.5",
"vite": "^5.2.0",
"concurrently": "^8.2.2"
}
}
+693
View File
@@ -0,0 +1,693 @@
import { createServer } from "http";
import { readFileSync, existsSync, writeFileSync, statSync } from "fs";
import { appendFile, mkdir, readFile } from "fs/promises";
// ---------- 配置加载 ----------
function loadEnv() {
const env: Record<string, string> = {};
try {
const raw = readFileSync(".env", "utf-8");
for (const line of raw.split("\n")) {
const t = line.trim();
if (!t || t.startsWith("#")) continue;
const idx = t.indexOf("=");
if (idx === -1) continue;
env[t.slice(0, idx).trim()] = t.slice(idx + 1).trim();
}
} catch {
/* ignore */
}
return env;
}
const ENV = loadEnv();
// 运行时可变配置(可通过 /api/settings 修改并持久化)
function parseList(v: string | undefined): string[] {
return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
}
const DEFAULT_WHITELIST = ["d:\\"];
const config = {
BASE_URL: process.env.BASE_URL || ENV.BASE_URL || "https://ark.cn-beijing.volces.com/api/coding/v3",
API_KEY: process.env.API_KEY || ENV.API_KEY || "",
MODEL: process.env.MODEL || ENV.MODEL || "",
WORKSPACE: process.env.WORKSPACE || ENV.WORKSPACE || process.cwd(),
CMD_APPROVE_LEVEL: (process.env.CMD_APPROVE_LEVEL || ENV.CMD_APPROVE_LEVEL || "medium").toLowerCase(),
PORT: parseInt(process.env.PORT || ENV.PORT || "3000", 10),
LOG_LEVEL: (process.env.LOG_LEVEL || ENV.LOG_LEVEL || "debug").toLowerCase(),
TEMPERATURE: parseFloat(process.env.TEMPERATURE || ENV.TEMPERATURE || "0.3"),
WHITELIST: parseList(process.env.WHITELIST || ENV.WHITELIST).length ? parseList(process.env.WHITELIST || ENV.WHITELIST) : DEFAULT_WHITELIST,
BLACKLIST: parseList(process.env.BLACKLIST || ENV.BLACKLIST),
};
// 持久化配置到 .env(WORKSPACE 为内部默认值,不在设置页暴露)
const ENV_KEYS = ["BASE_URL", "API_KEY", "MODEL", "WORKSPACE", "CMD_APPROVE_LEVEL", "PORT", "LOG_LEVEL", "TEMPERATURE", "WHITELIST", "BLACKLIST"] as const;
function saveEnv() {
const lines = ENV_KEYS.map((k) => `${k}=${String((config as any)[k])}`);
const header = "# Mini AI Assistant 配置(可由设置页面修改)\n";
try {
writeFileSync(".env", header + lines.join("\n") + "\n", "utf-8");
} catch (e) {
console.error("保存配置失败:", (e as any).message);
}
}
// ---------- 日志系统 ----------
const LOG_DIR = "logs";
const LEVELS: Record<string, number> = { debug: 0, info: 1, warn: 2, error: 3 };
function shouldLog(level: string): boolean {
const lv = LEVELS[level] ?? 1;
return lv >= (LEVELS[config.LOG_LEVEL] ?? 0);
}
async function log(level: string, category: string, data: any) {
if (!shouldLog(level)) return;
const ts = new Date().toISOString();
const text = typeof s === "string" ? s : safeStringify(data);
const line = `[${ts}] [${level.toUpperCase().padEnd(5)}] [${category}] ${text}\n`;
try {
if (!existsSync(LOG_DIR)) await mkdir(LOG_DIR, { recursive: true });
await appendFile(`${LOG_DIR}/chat-${ts.slice(0, 10)}.log`, line, "utf-8");
} catch {
/* 忽略日志写入失败 */
}
}
// 可安全序列化 BigInt 等类型
function safeStringify(s: any): string {
try {
return JSON.stringify(s, (_k, v) => (typeof v === "bigint" ? v.toString() : v), 0);
} catch {
try { return String(s); } catch { return "[unserializable]"; }
}
}
// 安全截断
function clip(s: any, n = 600): string {
const str = typeof s === "string" ? s : safeStringify(s);
return str.length > n ? str.slice(0, n) + "…" : str;
}
// ---------- 依赖 ----------
import { readFile, writeFile, readdir, rename, unlink, stat, mkdir } from "fs/promises";
import { resolve, normalize, extname, sep } from "path";
import { read as xlsxRead, write as xlsxWrite, utils } from "xlsx";
import duckdb from "duckdb";
// ---------- 路径安全(白名单 + 黑名单,黑名单优先) ----------
// 判断 target 是否等于 base 或位于 base 的子目录下(跨平台)
function isWithin(base: string, target: string): boolean {
const b = normalize(base).replace(/\\/g, "/").replace(/\/+$/, "");
const t = normalize(target).replace(/\\/g, "/").replace(/\/+$/, "");
return t === b || t.startsWith(b + "/");
}
// 白名单:允许访问的根目录(默认包含 D 盘根目录);工作区始终隐式允许
// 黑名单:禁止访问(优先于白名单);命中黑名单或其子目录则拒绝
function isAllowedPath(p: string): boolean {
const abs = resolve(config.WORKSPACE, normalize(p));
for (const b of config.BLACKLIST) {
if (isWithin(b, abs)) return false;
}
const allowed = [...config.WHITELIST, config.WORKSPACE];
for (const w of allowed) {
if (isWithin(w, abs)) return true;
}
return false;
}
// ---------- 危险命令检测 ----------
const DANGEROUS_PATTERNS = [
"rm -rf /", "rd /s", "format", "mkfs", "shutdown", "reboot", "poweroff",
"dd if=", "fdisk", ":(){:|:&};:", "chmod -r", "del /s", "attrib",
];
function isDangerousCommand(cmd: string): boolean {
const lower = cmd.toLowerCase();
return DANGEROUS_PATTERNS.some((p) => lower.includes(p));
}
// ---------- DuckDB(Excel 数据处理) ----------
let duckDb: any = null;
function getDuck(): Promise<any> {
return new Promise((resolve, reject) => {
if (duckDb) return resolve(duckDb);
duckDb = new duckdb.Database(":memory:", (err: any) => {
if (err) return reject(err);
resolve(duckDb);
});
});
}
function duckExec(db: any, sql: string): Promise<void> {
return new Promise((resolve, reject) => db.exec(sql, (e: any) => (e ? reject(e) : resolve())));
}
function duckAll(db: any, sql: string): Promise<any[]> {
return new Promise((resolve, reject) => db.all(sql, (e: any, rows: any) => (e ? reject(e) : resolve(rows || []))));
}
function sqlVal(v: any): string {
if (v === null || v === undefined || v === "") return "NULL";
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "NULL";
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
if (v instanceof Date) return `'${v.toISOString().replace(/'/g, "''")}'`;
return `'${String(v).replace(/'/g, "''")}'`;
}
function safeCol(c: string): string {
const s = String(c).trim() || "col";
return s.replace(/[^\w一-龥]/g, "_");
}
// ---------- 工具执行 ----------
async function execTool(name: string, args: any): Promise<{ ok: boolean; result: any }> {
try {
switch (name) {
case "read_file": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
const content = await readFile(resolve(config.WORKSPACE, normalize(args.path)), "utf-8");
return { ok: true, result: content.slice(0, 50000) };
}
case "write_file": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
const abs = resolve(config.WORKSPACE, normalize(args.path));
await mkdir(resolve(abs, ".."), { recursive: true }).catch(() => {});
await writeFile(abs, args.content ?? "", "utf-8");
return { ok: true, result: `已写入 ${args.path} (${((args.content ?? "").length)} 字符)` };
}
case "list_directory": {
const target = args.path ? normalize(args.path) : ".";
if (!isAllowedPath(target)) throw new Error("路径越权");
const entries = await readdir(resolve(config.WORKSPACE, target), { withFileTypes: true });
return {
ok: true,
result: entries.map((e) => ({ name: e.name, type: e.isDirectory() ? "dir" : "file" })),
};
}
case "copy_file": {
if (!isAllowedPath(args.source) || !isAllowedPath(args.destination)) throw new Error("路径越权");
const src = resolve(config.WORKSPACE, normalize(args.source));
const dst = resolve(config.WORKSPACE, normalize(args.destination));
const content = await readFile(src);
await mkdir(resolve(dst, ".."), { recursive: true }).catch(() => {});
await writeFile(dst, content);
return { ok: true, result: `已复制 ${args.source} -> ${args.destination}` };
}
case "move_file": {
if (!isAllowedPath(args.source) || !isAllowedPath(args.destination)) throw new Error("路径越权");
const src = resolve(config.WORKSPACE, normalize(args.source));
const dst = resolve(config.WORKSPACE, normalize(args.destination));
await mkdir(resolve(dst, ".."), { recursive: true }).catch(() => {});
await rename(src, dst);
return { ok: true, result: `已移动 ${args.source} -> ${args.destination}` };
}
case "delete_file": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
await unlink(resolve(config.WORKSPACE, normalize(args.path)));
return { ok: true, result: `已删除 ${args.path}` };
}
case "execute_command": {
const { spawn } = await import("child_process");
const out = await new Promise<{ success: boolean; output: string; error: string }>((resolve) => {
const proc = spawn(args.command, { cwd: config.WORKSPACE, shell: true, env: { ...process.env } });
let stdout = "", stderr = "";
proc.stdout?.on("data", (d) => (stdout += d.toString()));
proc.stderr?.on("data", (d) => (stderr += d.toString()));
proc.on("close", (code) => resolve({ success: code === 0, output: stdout, error: stderr }));
proc.on("error", (e) => resolve({ success: false, output: "", error: e.message }));
});
return { ok: out.success, result: out.output || out.error };
}
case "read_excel": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
const buf = await readFile(resolve(config.WORKSPACE, normalize(args.path)));
const wb = xlsxRead(buf, { type: "buffer" });
const sheetName = args.sheet || wb.SheetNames[0];
const ws = wb.Sheets[sheetName];
const rows = utils.sheet_to_json(ws, { header: 1 });
const limit = args.limit || 200;
return {
ok: true,
result: { totalRows: rows.length, columns: rows[0] || [], data: rows.slice(0, limit) },
};
}
case "write_excel": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
const rows = (args.data || []).map((o: any) => Object.values(o));
const headers = args.data?.length ? Object.keys(args.data[0]) : [];
const ws = utils.aoa_to_sheet([headers, ...rows]);
const wb = { Sheets: {}, SheetNames: [] } as any;
wb.SheetNames.push(args.sheet || "Sheet1");
wb.Sheets[wb.SheetNames[0]] = ws;
await mkdir(resolve(config.WORKSPACE, normalize(args.path), ".."), { recursive: true }).catch(() => {});
const out = xlsxWrite(wb, { type: "buffer", bookType: "xlsx" });
writeFileSync(resolve(config.WORKSPACE, normalize(args.path)), out);
return { ok: true, result: `已写入 Excel: ${args.path} (${rows.length} 行)` };
}
case "load_excel": {
if (!isAllowedPath(args.path)) throw new Error("路径越权");
const buf = await readFile(resolve(config.WORKSPACE, normalize(args.path)));
const wb = xlsxRead(buf, { type: "buffer" });
const sheetName = args.sheet || wb.SheetNames[0];
const ws = wb.Sheets[sheetName];
if (!ws) throw new Error("工作表不存在: " + sheetName);
const rows: any[] = utils.sheet_to_json(ws, { defval: null });
const table = args.table || "excel_data";
const db = await getDuck();
if (!rows.length) return { ok: true, result: `工作表 "${sheetName}" 无数据行` };
const rawCols = Object.keys(rows[0]);
const seen = new Set<string>();
const cols = rawCols.map((c, i) => {
let name = String(c).trim() === "" ? `col_${i}` : c;
let n = name, k = 1;
while (seen.has(n)) n = `${name}_${k++}`;
seen.add(n);
return n;
});
// 类型推断:整列均为数字 → DOUBLE,否则 VARCHAR
const types: Record<string, string> = {};
for (const c of cols) {
let allNum = true, hasVal = false;
for (const r of rows) {
const v = r[c];
if (v === null || v === undefined || v === "") continue;
hasVal = true;
if (typeof v !== "number") { allNum = false; break; }
}
types[c] = hasVal && allNum ? "DOUBLE" : "VARCHAR";
}
const colDefs = cols.map((c) => `"${c}" ${types[c]}`).join(", ");
await duckExec(db, `CREATE OR REPLACE TABLE "${table}" (${colDefs})`);
const MAX = 50000;
const slice = rows.slice(0, MAX);
const values = slice.map((r) => `(${cols.map((c) => sqlVal(r[c])).join(", ")})`).join(", ");
if (values) await duckExec(db, `INSERT INTO "${table}" VALUES ${values}`);
return {
ok: true,
result: {
table, sheet: sheetName, rowCount: rows.length, truncated: rows.length > MAX,
columns: cols.map((c) => ({ name: c, type: types[c] })),
preview: slice.slice(0, 3),
note: `已加载为 DuckDB 表 "${table}",可用 query_duckdb 执行 SQL;列名用双引号引用,例如 SELECT * FROM "${table}" LIMIT 10`,
},
};
}
case "query_duckdb": {
const sql = String(args.sql || "").trim();
if (!sql) throw new Error("缺少 SQL 语句");
const db = await getDuck();
try {
const rows = await duckAll(db, sql);
const cols = rows.length ? Object.keys(rows[0]) : [];
return { ok: true, result: { rowCount: rows.length, columns: cols, data: rows.slice(0, 200) } };
} catch (e: any) {
return { ok: false, result: "SQL 执行失败: " + e.message };
}
}
default:
return { ok: false, result: "未知工具: " + name };
}
} catch (e: any) {
return { ok: false, result: "执行失败: " + e.message };
}
}
// 需要审批的工具:delete_file 始终需要;execute_command 依据授权级别
const NEEDS_APPROVAL = new Set(["delete_file"]);
// 命令授权级别:high=每个命令都审核, medium=仅危险命令审核, low=全部不审核(默认 medium)
function needsApproval(name: string, args: any): boolean {
if (NEEDS_APPROVAL.has(name)) return true;
if (name === "execute_command") {
const lv = config.CMD_APPROVE_LEVEL;
if (lv === "high") return true;
if (lv === "low") return false;
return isDangerousCommand(String(args?.command || ""));
}
return false;
}
// ---------- 工具定义 (OpenAI function calling) ----------
const TOOL_DEFINITIONS = [
{ type: "function", function: { name: "read_file", description: "读取文本文件内容", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } } },
{ type: "function", function: { name: "write_file", description: "写入文本内容到文件(覆盖)", parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } } },
{ type: "function", function: { name: "list_directory", description: "列出目录内容", parameters: { type: "object", properties: { path: { type: "string" } } } } },
{ type: "function", function: { name: "copy_file", description: "复制文件或目录", parameters: { type: "object", properties: { source: { type: "string" }, destination: { type: "string" } }, required: ["source", "destination"] } } },
{ type: "function", function: { name: "move_file", description: "移动/重命名文件或目录", parameters: { type: "object", properties: { source: { type: "string" }, destination: { type: "string" } }, required: ["source", "destination"] } } },
{ type: "function", function: { name: "delete_file", description: "删除文件或空目录(需审批)", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } } },
{ type: "function", function: { name: "execute_command", description: "执行 Shell 命令(需审批)", parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } } },
{ type: "function", function: { name: "read_excel", description: "读取 Excel 返回 JSON(默认前 200 行)", parameters: { type: "object", properties: { path: { type: "string" }, sheet: { type: "string" }, limit: { type: "number" } }, required: ["path"] } } },
{ type: "function", function: { name: "write_excel", description: "将 JSON 数组写入 Excel", parameters: { type: "object", properties: { path: { type: "string" }, data: { type: "array" }, sheet: { type: "string" } }, required: ["path", "data"] } } },
{ type: "function", function: { name: "load_excel", description: "将 Excel 文件加载进 DuckDB 内存表以便用 SQL 分析数据处理。返回表名、列名与类型、前几行预览。加载后即可用 query_duckdb 执行 SQL(筛选/聚合/排序/连接等)。", parameters: { type: "object", properties: { path: { type: "string", description: "Excel 文件路径" }, sheet: { type: "string", description: "工作表名,默认第一个" }, table: { type: "string", description: "目标表名,默认 excel_data" } }, required: ["path"] } } },
{ type: "function", function: { name: "query_duckdb", description: "在已加载的 DuckDB 表上执行 SQL 查询(SELECT 等)。列名用双引号引用,例如 SELECT \"姓名\", SUM(\"金额\") FROM excel_data GROUP BY \"姓名\"。", parameters: { type: "object", properties: { sql: { type: "string", description: "要执行的 SQL 语句" } }, required: ["sql"] } } },
];
// ---------- LLM 调用 (流式) ----------
interface ChatMsg { role: string; content: any; tool_calls?: any[]; name?: string; }
async function* streamLLM(messages: ChatMsg[]): AsyncGenerator<{ type: "token" | "toolcall" | "error"; content?: string; toolcall?: any }> {
const url = config.BASE_URL.replace(/\/$/, "") + "/chat/completions";
const body: any = {
messages: messages.map((m) => ({
role: m.role,
content: m.content,
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
...(m.name ? { name: m.name } : {}),
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
})),
tools: TOOL_DEFINITIONS,
tool_choice: "auto",
stream: true,
temperature: config.TEMPERATURE,
};
if (config.MODEL) body.model = config.MODEL;
let resp: any;
try {
resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.API_KEY}` },
body: JSON.stringify(body),
});
} catch (e: any) {
yield { type: "error", content: "无法连接模型服务: " + e.message };
return;
}
if (!resp.ok) {
const txt = await resp.text().catch(() => "");
yield { type: "error", content: `模型返回错误 (${resp.status}): ${txt.slice(0, 300)}` };
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let toolAcc: any = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const t = line.trim();
if (!t.startsWith("data:")) continue;
const data = t.slice(5).trim();
if (data === "[DONE]") continue;
let json: any;
try { json = JSON.parse(data); } catch { continue; }
const delta = json.choices?.[0]?.delta || {};
if (delta.content) yield { type: "token", content: delta.content };
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
if (!toolAcc) toolAcc = { id: "", type: "function", function: { name: "", arguments: "" } };
if (tc.id) toolAcc.id = tc.id;
if (tc.function?.name) toolAcc.function.name = tc.function.name;
if (tc.function?.arguments) toolAcc.function.arguments += tc.function.arguments;
}
}
}
}
if (toolAcc) {
if (!toolAcc.id) toolAcc.id = "call_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7);
yield { type: "toolcall", toolcall: toolAcc };
}
}
// ---------- SSE 辅助 ----------
function sseInit(res: any) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
});
}
function sseSend(res: any, obj: any) {
res.write(`data: ${JSON.stringify(obj)}\n\n`);
}
// ---------- 对话处理(带工具循环) ----------
let pendingApproval: { messages: ChatMsg[]; toolCall: any } | null = null;
// 系统提示:定义助手身份
const SYSTEM_PROMPT = "你是小鑫,一个专为冯雅鑫定制的本地 AI 助手,可以帮助处理文件读写、Excel、列出目录、执行命令(需审批)等任务。分析 Excel 数据时,优先用 load_excel 把文件加载进 DuckDB,再用 query_duckdb 执行 SQL 完成筛选、聚合、统计、排序等;处理大量数据时不要逐行读取。请用简体中文友好、简洁地回答。";
function withSystem(msgs: ChatMsg[]): ChatMsg[] {
if (msgs.length && msgs[0].role === "system") return msgs;
return [{ role: "system", content: SYSTEM_PROMPT }, ...msgs];
}
async function runConversation(initialMessages: ChatMsg[], res: any, fromApproval = false) {
sseInit(res);
let messages = withSystem(initialMessages);
// 日志:记录用户/工具输入
log("debug", "conversation_start", {
fromApproval,
messages: messages.map((m) => ({ role: m.role, content: clip(m.content, 800) })),
});
// 工具循环(最多 5 轮避免死循环)
for (let round = 0; round < 5; round++) {
const gen = streamLLM(messages);
let content = "";
let toolCall: any = null;
let errored = false;
for await (const ev of gen) {
if (ev.type === "token") {
content += ev.content;
sseSend(res, { type: "token", content: ev.content });
} else if (ev.type === "toolcall") {
toolCall = ev.toolcall;
} else if (ev.type === "error") {
errored = true;
log("error", "llm_error", ev.content);
sseSend(res, { type: "token", content: "\n\n⚠️ " + ev.content });
}
}
if (errored) { sseSend(res, { type: "done" }); res.end(); return; }
// 需要工具调用
if (toolCall && toolCall.function?.name) {
const name = toolCall.function.name;
let args = {};
try { args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}; } catch { args = {}; }
if (needsApproval(name, args)) {
// 请求审批,保存上下文等待 /api/approve
log("info", "approval_required", { tool: name, args });
pendingApproval = {
messages: [
...messages,
{ role: "assistant", content: content || null, tool_calls: [toolCall] },
],
toolCall,
};
sseSend(res, {
type: "approval_required",
tool_call_id: toolCall.id,
tool: name,
args,
command: name === "execute_command" ? args.command : `${name}(${JSON.stringify(args)})`,
});
sseSend(res, { type: "done" });
res.end();
return;
}
// 直接执行
log("debug", "tool_call", { name, args });
const exec = await execTool(name, args);
log("debug", "tool_result", { name, ok: exec.ok, result: clip(exec.result, 800) });
messages = [
...messages,
{ role: "assistant", content: content || null, tool_calls: [toolCall] },
{ role: "tool", name, content: safeStringify(exec), tool_call_id: toolCall.id },
];
// 继续循环,让模型基于工具结果生成最终回复
continue;
}
// 无工具调用,正常结束
log("debug", "assistant", clip(content, 2000));
sseSend(res, { type: "done" });
res.end();
return;
}
sseSend(res, { type: "token", content: "\n[达到最大工具调用轮数]" });
sseSend(res, { type: "done" });
res.end();
}
// ---------- 静态文件服务(生产构建) ----------
const DIST = resolve("dist");
const MIME: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".map": "application/json",
};
async function serveStatic(res: any, url: URL): Promise<boolean> {
if (!existsSync(DIST) || !statSync(DIST).isDirectory()) return false;
let pathname = decodeURIComponent(url.pathname);
if (pathname === "/") pathname = "/index.html";
const safePath = resolve(DIST, "." + pathname);
if (safePath !== DIST && !safePath.startsWith(DIST + sep)) {
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Forbidden");
return true;
}
try {
if (existsSync(safePath) && statSync(safePath).isFile()) {
const ext = extname(safePath).toLowerCase();
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
res.end(await readFile(safePath));
return true;
}
const idx = resolve(DIST, "index.html");
if (existsSync(idx)) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(await readFile(idx));
return true;
}
} catch {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Internal error");
return true;
}
return false;
}
// ---------- HTTP 服务 ----------
const server = createServer(async (req, res) => {
const url = new URL(req.url || "", `http://localhost:${config.PORT}`);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
if (req.method === "GET" && !url.pathname.startsWith("/api")) {
if (await serveStatic(res, url)) return;
}
if (req.method === "GET" && url.pathname === "/") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", model: config.MODEL || "default", workspace: config.WORKSPACE }));
return;
}
// 读取设置
if (req.method === "GET" && url.pathname === "/api/settings") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
BASE_URL: config.BASE_URL,
API_KEY: config.API_KEY ? "******" + config.API_KEY.slice(-4) : "",
MODEL: config.MODEL,
CMD_APPROVE_LEVEL: config.CMD_APPROVE_LEVEL,
LOG_LEVEL: config.LOG_LEVEL,
TEMPERATURE: config.TEMPERATURE,
WHITELIST: config.WHITELIST,
BLACKLIST: config.BLACKLIST,
}));
return;
}
// 保存设置
if (req.method === "POST" && url.pathname === "/api/settings") {
let body = "";
for await (const c of req) body += c;
let payload: any;
try { payload = JSON.parse(body); } catch { res.writeHead(400); res.end("bad json"); return; }
if (typeof payload.BASE_URL === "string") config.BASE_URL = payload.BASE_URL;
if (typeof payload.API_KEY === "string" && !payload.API_KEY.startsWith("******")) config.API_KEY = payload.API_KEY;
if (typeof payload.MODEL === "string") config.MODEL = payload.MODEL;
if (payload.CMD_APPROVE_LEVEL === "high" || payload.CMD_APPROVE_LEVEL === "medium" || payload.CMD_APPROVE_LEVEL === "low") {
config.CMD_APPROVE_LEVEL = payload.CMD_APPROVE_LEVEL;
}
if (typeof payload.LOG_LEVEL === "string") config.LOG_LEVEL = payload.LOG_LEVEL.toLowerCase();
if (typeof payload.TEMPERATURE === "number") config.TEMPERATURE = payload.TEMPERATURE;
if (payload.WHITELIST !== undefined) {
const wl = Array.isArray(payload.WHITELIST) ? payload.WHITELIST : String(payload.WHITELIST).split(",");
config.WHITELIST = wl.map((s: string) => String(s).trim()).filter(Boolean);
}
if (payload.BLACKLIST !== undefined) {
const bl = Array.isArray(payload.BLACKLIST) ? payload.BLACKLIST : String(payload.BLACKLIST).split(",");
config.BLACKLIST = bl.map((s: string) => String(s).trim()).filter(Boolean);
}
saveEnv();
log("info", "settings_updated", { CMD_APPROVE_LEVEL: config.CMD_APPROVE_LEVEL, MODEL: config.MODEL, LOG_LEVEL: config.LOG_LEVEL, WHITELIST: config.WHITELIST, BLACKLIST: config.BLACKLIST });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
// 聊天:流式 SSE
if (req.method === "POST" && url.pathname === "/api/chat") {
let body = "";
for await (const c of req) body += c;
let payload: any;
try { payload = JSON.parse(body); } catch { res.writeHead(400); res.end("bad json"); return; }
const history: ChatMsg[] = (payload.messages || []).map((m: any) => ({
role: m.role,
content: m.content,
}));
await runConversation(history, res);
return;
}
// 审批反馈:流式 SSE(继续对话)
if (req.method === "POST" && url.pathname === "/api/approve") {
let body = "";
for await (const c of req) body += c;
let payload: any;
try { payload = JSON.parse(body); } catch { res.writeHead(400); res.end("bad json"); return; }
const approved = !!payload.approved;
if (!pendingApproval) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "无待审批操作" }));
return;
}
const { messages, toolCall } = pendingApproval;
pendingApproval = null;
const name = toolCall.function.name;
let args = {};
try { args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}; } catch {}
let exec;
if (approved) {
log("info", "approval_granted", { tool: name, args });
exec = await execTool(name, args);
} else {
log("info", "approval_denied", { tool: name, args });
exec = { ok: false, result: "用户已拒绝该操作" };
}
const nextMessages: ChatMsg[] = [
...messages,
{ role: "tool", name, content: safeStringify(exec), tool_call_id: toolCall.id },
];
await runConversation(nextMessages, res, true);
return;
}
res.writeHead(404);
res.end("Not Found");
});
server.listen(config.PORT, () => {
console.log(`Mini AI Assistant 后端已启动`);
console.log(` API: http://127.0.0.1:${config.PORT}`);
console.log(` 模型: ${config.BASE_URL}`);
console.log(` 工作区: ${config.WORKSPACE}`);
console.log(` 日志级别: ${config.LOG_LEVEL} → logs/`);
});
+303
View File
@@ -0,0 +1,303 @@
<template>
<div class="app" :class="theme">
<header class="topbar">
<div class="brand">
<div class="logo"><AssistantAvatar /></div>
<div>
<div class="brand-title">小鑫 AI 助手</div>
<div class="brand-sub">专为冯雅鑫定制</div>
</div>
</div>
<div class="actions">
<button class="icon-btn" title="设置" @click="showSettings = true">⚙️</button>
<button class="icon-btn" title="清空对话" @click="clearChat">🗑️</button>
<button class="icon-btn" :title="theme === 'dark' ? '浅色模式' : '深色模式'" @click="toggleTheme">
{{ theme === 'dark' ? '☀️' : '🌙' }}
</button>
</div>
</header>
<main class="chat" ref="chatEl">
<div v-if="messages.length === 0" class="empty">
<div class="empty-icon">💬</div>
<h2>开始对话</h2>
<p>你可以让我读取文件、处理 Excel、执行命令(需审批)等。</p>
<div class="suggest">
<button v-for="s in suggestions" :key="s" @click="send(s)">{{ s }}</button>
</div>
</div>
<MessageList
v-else
:messages="messages"
@approve="onApprove"
@reject="onReject"
/>
</main>
<InputArea :disabled="busy" @send="send" />
<SettingsModal v-if="showSettings" :api-base="API_BASE" @close="showSettings = false" />
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from "vue";
import MessageList from "./components/MessageList.vue";
import InputArea from "./components/InputArea.vue";
import SettingsModal from "./components/SettingsModal.vue";
import AssistantAvatar from "./components/AssistantAvatar.vue";
const showSettings = ref(false);
type Role = "user" | "assistant" | "system";
interface Msg {
id: number;
role: Role;
content: string;
loading?: boolean;
approval?: {
tool: string;
args: any;
command: string;
tool_call_id: string;
};
}
const messages = ref<Msg[]>([]);
const theme = ref<"light" | "dark">("light");
const busy = ref(false);
const chatEl = ref<HTMLElement | null>(null);
const suggestions = [
"列出当前目录的文件",
"读取 data.xlsx 并告诉我前两行",
"帮我创建一个 hello.txt 写入你好",
];
let idSeq = 1;
const scrollToBottom = async () => {
await nextTick();
chatEl.value?.scrollTo({ top: chatEl.value.scrollHeight, behavior: "smooth" });
};
function pushMsg(role: Role, content = ""): Msg {
const m: Msg = { id: idSeq++, role, content };
messages.value.push(m);
return messages.value[messages.value.length - 1];
}
// API 地址:开发环境直连后端(绕过 Vite 代理以保证 SSE 流式正常),生产可改为相对路径
const API_BASE: string = (import.meta as any).env?.VITE_API_BASE || (import.meta as any).env?.DEV ? "http://127.0.0.1:3100" : "";
// 读取 SSE 流
async function consumeStream(response: Response, target: Msg) {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() || "";
for (const line of lines) {
const t = line.trim();
if (!t.startsWith("data:")) continue;
const data = t.slice(5).trim();
let ev: any;
try { ev = JSON.parse(data); } catch { continue; }
if (ev.type === "token" && ev.content) {
target.content += ev.content;
scrollToBottom();
} else if (ev.type === "approval_required") {
target.loading = false;
target.approval = {
tool: ev.tool,
args: ev.args,
command: ev.command,
tool_call_id: ev.tool_call_id,
};
busy.value = false;
return;
} else if (ev.type === "done") {
target.loading = false;
busy.value = false;
scrollToBottom();
return;
}
}
}
target.loading = false;
busy.value = false;
}
async function send(text: string) {
const content = text.trim();
if (!content || busy.value) return;
busy.value = true;
pushMsg("user", content);
const assistant = pushMsg("assistant", "");
assistant.loading = true;
scrollToBottom();
try {
const resp = await fetch(`${API_BASE}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: messages.value
.filter((m) => !m.approval && m.content && m.role !== "assistant")
.map((m) => ({ role: m.role, content: m.content })),
}),
});
await consumeStream(resp, assistant);
} catch (e: any) {
assistant.content = "⚠️ 请求失败:" + e.message;
assistant.loading = false;
busy.value = false;
}
}
async function handleDecision(approved: boolean, m: Msg) {
if (!m.approval) return;
const toolCallId = m.approval.tool_call_id;
// 移除审批卡片消息,避免留下空气泡
messages.value = messages.value.filter((x) => x.id !== m.id);
busy.value = true;
const assistant = pushMsg("assistant", "");
assistant.loading = true;
scrollToBottom();
try {
const resp = await fetch(`${API_BASE}/api/approve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approved, tool_call_id: toolCallId }),
});
await consumeStream(resp, assistant);
} catch (e: any) {
assistant.content = "⚠️ 请求失败:" + e.message;
assistant.loading = false;
busy.value = false;
}
}
const onApprove = (m: Msg) => handleDecision(true, m);
const onReject = (m: Msg) => handleDecision(false, m);
function clearChat() {
messages.value = [];
}
function toggleTheme() {
theme.value = theme.value === "dark" ? "light" : "dark";
document.documentElement.classList.toggle("dark", theme.value === "dark");
localStorage.setItem("theme", theme.value);
}
onMounted(() => {
const saved = localStorage.getItem("theme");
if (saved === "dark") {
theme.value = "dark";
document.documentElement.classList.add("dark");
}
});
</script>
<style>
:root {
--bg: #f4f6fb;
--surface: #ffffff;
--surface-2: #f0f2f7;
--text: #1f2733;
--text-dim: #6b7686;
--border: #e6e9f0;
--primary: #4f6ef7;
--primary-hover: #3d5be0;
--user-bubble: #4f6ef7;
--user-text: #ffffff;
--assistant-bubble: #ffffff;
--code-bg: #1e2430;
--code-text: #e6edf3;
--shadow: 0 1px 3px rgba(20, 30, 50, 0.08), 0 8px 24px rgba(20, 30, 50, 0.06);
--radius: 16px;
}
.dark {
--bg: #0f141b;
--surface: #171d27;
--surface-2: #1e2530;
--text: #e6eaf0;
--text-dim: #8a93a3;
--border: #2a323f;
--primary: #5b7cfa;
--primary-hover: #6f8cff;
--user-bubble: #3d5be0;
--user-text: #ffffff;
--assistant-bubble: #1e2530;
--code-bg: #0b0e14;
--code-text: #e6edf3;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3);
}
* { box-sizing: border-box; }
html, body, #app { height: 100%; margin: 0; }
body {
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
}
</style>
<style scoped>
.app {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 880px;
margin: 0 auto;
background: var(--bg);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
z-index: 5;
}
.brand { display: flex; align-items: center; gap: 12px; }
.logo {
width: 40px; height: 40px; border-radius: 12px; overflow: hidden;
display: grid; place-items: center; background: transparent; padding: 0;
}
.brand-title { font-weight: 700; font-size: 16px; }
.brand-sub { font-size: 12px; color: var(--text-dim); }
.actions { display: flex; gap: 8px; }
.icon-btn {
width: 38px; height: 38px; border-radius: 10px;
border: 1px solid var(--border); background: var(--surface-2);
cursor: pointer; font-size: 16px; transition: 0.15s;
}
.icon-btn:hover { background: var(--border); }
.chat {
flex: 1; overflow-y: auto; padding: 24px 20px 8px;
display: flex; flex-direction: column; gap: 18px;
}
.empty {
margin: auto; text-align: center; color: var(--text-dim); max-width: 420px;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; }
.empty h2 { color: var(--text); margin: 0 0 8px; }
.suggest { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 18px; }
.suggest button {
background: var(--surface); border: 1px solid var(--border);
color: var(--text); padding: 8px 14px; border-radius: 20px;
cursor: pointer; font-size: 13px; transition: 0.15s;
}
.suggest button:hover { border-color: var(--primary); color: var(--primary); }
</style>
+47
View File
@@ -0,0 +1,47 @@
<template>
<svg class="ava" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="小鑫">
<defs>
<linearGradient id="xiaoxin-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#8a5cf6" />
<stop offset="100%" stop-color="#4f6ef7" />
</linearGradient>
<linearGradient id="xiaoxin-hair" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6b4226" />
<stop offset="100%" stop-color="#3d261a" />
</linearGradient>
</defs>
<!-- 背景 -->
<rect width="48" height="48" rx="10" fill="url(#xiaoxin-bg)" />
<!-- 头发(外层) -->
<path d="M9 27 C9 12 39 12 39 27 C39 36 34 42 24 42 C14 42 9 36 9 27 Z" fill="url(#xiaoxin-hair)" />
<!-- 脸 -->
<circle cx="24" cy="25" r="11.5" fill="#ffe2c4" />
<!-- 刘海 -->
<path d="M12.5 25 C12.5 15.5 35.5 15.5 35.5 25 C35.5 20.5 30 17.5 24 17.5 C18 17.5 12.5 20.5 12.5 25 Z" fill="url(#xiaoxin-hair)" />
<!-- 双马尾 -->
<circle cx="10.5" cy="27" r="4.2" fill="url(#xiaoxin-hair)" />
<circle cx="37.5" cy="27" r="4.2" fill="url(#xiaoxin-hair)" />
<!-- 眼睛 -->
<circle cx="19.5" cy="25" r="1.9" fill="#2b2b2b" />
<circle cx="28.5" cy="25" r="1.9" fill="#2b2b2b" />
<circle cx="20.1" cy="24.4" r="0.6" fill="#fff" />
<circle cx="29.1" cy="24.4" r="0.6" fill="#fff" />
<!-- 腮红 -->
<circle cx="17.5" cy="29" r="2" fill="#ffb3b3" opacity="0.55" />
<circle cx="30.5" cy="29" r="2" fill="#ffb3b3" opacity="0.55" />
<!-- 微笑 -->
<path d="M21 30.5 Q24 33.5 27 30.5" stroke="#c2462f" stroke-width="1.6" fill="none" stroke-linecap="round" />
<!-- AI 头戴耳机 -->
<path d="M11.5 24 a12.5 12.5 0 0 1 25 0" stroke="#eef1ff" stroke-width="2.6" fill="none" stroke-linecap="round" />
<rect x="8.5" y="22" width="4.4" height="7.5" rx="2.2" fill="#eef1ff" />
<circle cx="34" cy="25.5" r="3.2" fill="#eef1ff" />
<rect x="32.8" y="28" width="2.2" height="6" rx="1.1" fill="#eef1ff" />
<circle cx="34" cy="25.5" r="1.3" fill="#4f6ef7" />
</svg>
</template>
<script setup lang="ts"></script>
<style scoped>
.ava { width: 100%; height: 100%; display: block; }
</style>
+72
View File
@@ -0,0 +1,72 @@
<template>
<div class="input-bar">
<textarea
ref="ta"
v-model="text"
class="ta"
:disabled="disabled"
rows="1"
placeholder="输入消息,Enter 发送,Shift+Enter 换行"
@input="autoGrow"
@keydown.enter.exact.prevent="submit"
></textarea>
<button class="send" :disabled="disabled || !text.trim()" @click="submit">
<span v-if="!disabled">➤</span>
<span v-else class="spinner"></span>
</button>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from "vue";
defineProps<{ disabled?: boolean }>();
const emit = defineEmits<{ send: [text: string] }>();
const text = ref("");
const ta = ref<HTMLTextAreaElement | null>(null);
function autoGrow() {
const el = ta.value;
if (!el) return;
el.style.height = "auto";
el.style.height = Math.min(el.scrollHeight, 160) + "px";
}
function submit() {
const v = text.value.trim();
if (!v) return;
emit("send", v);
text.value = "";
nextTick(autoGrow);
}
</script>
<style scoped>
.input-bar {
display: flex; align-items: flex-end; gap: 10px;
padding: 14px 20px;
background: var(--surface);
border-top: 1px solid var(--border);
}
.ta {
flex: 1; resize: none; border: 1px solid var(--border);
background: var(--surface-2); color: var(--text);
border-radius: 14px; padding: 12px 16px; font: inherit; font-size: 14.5px;
line-height: 1.5; max-height: 160px; outline: none; transition: border-color 0.15s;
}
.ta:focus { border-color: var(--primary); }
.ta:disabled { opacity: 0.6; }
.send {
width: 44px; height: 44px; flex-shrink: 0; border: none; border-radius: 12px;
background: var(--primary); color: #fff; cursor: pointer; font-size: 16px;
display: grid; place-items: center; transition: 0.15s;
}
.send:hover:not(:disabled) { background: var(--primary-hover); }
.send:disabled { background: var(--text-dim); cursor: not-allowed; }
.spinner {
width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.4);
border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
+125
View File
@@ -0,0 +1,125 @@
<template>
<div class="bubble" :class="msg.role">
<!-- 审批卡片 -->
<div v-if="msg.approval" class="approval">
<div class="approval-head">⚠️ 需要审批的操作</div>
<div class="approval-tool">{{ toolLabel }}</div>
<pre class="approval-cmd">{{ msg.approval.command }}</pre>
<div class="approval-btns">
<button class="allow" @click="$emit('approve')">✅ 允许</button>
<button class="deny" @click="$emit('reject')">❌ 拒绝</button>
</div>
</div>
<!-- 普通消息(Markdown) -->
<div v-else class="md" v-html="rendered"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { marked } from "marked";
import hljs from "highlight.js";
interface Msg {
id: number;
role: "user" | "assistant" | "system";
content: string;
approval?: { tool: string; command: string; args: any };
}
const props = defineProps<{ msg: Msg }>();
defineEmits<{ approve: []; reject: [] }>();
marked.setOptions({
highlight(code: string, lang: string) {
if (lang && hljs.getLanguage(lang)) return hljs.highlight(code, { language: lang }).value;
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true,
});
const rendered = computed(() => {
if (!props.msg.content) return "";
try {
return marked.parse(props.msg.content) as string;
} catch {
return props.msg.content;
}
});
const toolLabel = computed(() => {
const map: Record<string, string> = {
delete_file: "删除文件",
execute_command: "执行命令",
};
return map[props.msg.approval?.tool || ""] || props.msg.approval?.tool || "";
});
</script>
<style scoped>
.bubble { font-size: 14.5px; line-height: 1.7; }
.bubble.user :deep(.md) {
background: var(--user-bubble); color: var(--user-text);
padding: 10px 14px; border-radius: 14px 14px 4px 14px;
border-top-right-radius: 4px; word-break: break-word;
}
.bubble.assistant :deep(.md) {
background: var(--assistant-bubble); color: var(--text);
padding: 12px 16px; border-radius: 14px 14px 14px 4px;
border: 1px solid var(--border); box-shadow: var(--shadow);
word-break: break-word;
}
/* Markdown 元素 */
.bubble :deep(p) { margin: 0.5em 0; }
.bubble :deep(p:first-child) { margin-top: 0; }
.bubble :deep(p:last-child) { margin-bottom: 0; }
.bubble :deep(h1), .bubble :deep(h2), .bubble :deep(h3) { margin: 0.6em 0 0.4em; line-height: 1.3; }
.bubble :deep(ul), .bubble :deep(ol) { margin: 0.4em 0; padding-left: 1.4em; }
.bubble :deep(li) { margin: 0.2em 0; }
.bubble :deep(a) { color: var(--primary); text-decoration: none; }
.bubble :deep(a:hover) { text-decoration: underline; }
.bubble :deep(code):not(pre code) {
background: var(--surface-2); padding: 1px 6px; border-radius: 5px;
font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.88em;
}
.bubble :deep(pre) {
background: var(--code-bg); color: var(--code-text);
padding: 14px 16px; border-radius: 10px; overflow-x: auto;
margin: 0.6em 0; position: relative;
}
.bubble :deep(pre code) { font-family: "SFMono-Regular", Consolas, monospace; font-size: 13px; line-height: 1.6; }
.bubble :deep(table) {
border-collapse: collapse; margin: 0.6em 0; font-size: 13px; width: 100%;
}
.bubble :deep(th), .bubble :deep(td) { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.bubble :deep(th) { background: var(--surface-2); font-weight: 600; }
.bubble :deep(blockquote) {
border-left: 3px solid var(--primary); margin: 0.5em 0; padding: 2px 12px; color: var(--text-dim);
}
/* 审批卡片 */
.approval {
background: var(--surface); border: 1px solid #f0a020;
border-radius: 14px; padding: 14px 16px; box-shadow: var(--shadow);
max-width: 460px;
}
.approval-head { font-weight: 700; color: #d97706; margin-bottom: 8px; }
.approval-tool { font-size: 13px; color: var(--text-dim); margin-bottom: 6px; }
.approval-cmd {
background: var(--code-bg); color: var(--code-text); padding: 10px 12px;
border-radius: 8px; font-family: monospace; font-size: 12.5px;
white-space: pre-wrap; word-break: break-all; margin: 0 0 12px;
}
.approval-btns { display: flex; gap: 10px; }
.approval-btns button {
flex: 1; padding: 9px; border: none; border-radius: 9px; cursor: pointer;
font-weight: 600; font-size: 13px; transition: 0.15s;
}
.approval-btns .allow { background: #16a34a; color: #fff; }
.approval-btns .allow:hover { background: #15803d; }
.approval-btns .deny { background: #dc2626; color: #fff; }
.approval-btns .deny:hover { background: #b91c1c; }
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<div class="list">
<div v-for="m in messages" :key="m.id" class="row" :class="m.role">
<div class="avatar" :class="m.role">
<AssistantAvatar v-if="m.role === 'assistant'" />
<template v-else>🧑</template>
</div>
<div class="col">
<div class="meta">{{ m.role === "user" ? "你" : "小鑫" }}</div>
<MessageBubble :msg="m" @approve="() => $emit('approve', m)" @reject="() => $emit('reject', m)" />
<div v-if="m.loading" class="typing"><span></span><span></span><span></span></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import MessageBubble from "./MessageBubble.vue";
import AssistantAvatar from "./AssistantAvatar.vue";
interface Msg {
id: number;
role: "user" | "assistant" | "system";
content: string;
loading?: boolean;
approval?: any;
}
defineProps<{ messages: Msg[] }>();
defineEmits<{ approve: [m: Msg]; reject: [m: Msg] }>();
</script>
<style scoped>
.list { display: flex; flex-direction: column; gap: 18px; }
.row { display: flex; gap: 12px; align-items: flex-start; }
.row.user { flex-direction: row-reverse; }
.avatar {
width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0;
display: grid; place-items: center; font-size: 18px;
background: var(--surface-2); border: 1px solid var(--border);
}
.avatar.assistant { background: transparent; border: none; overflow: hidden; padding: 0; }
.col { display: flex; flex-direction: column; gap: 4px; max-width: 78%; }
.row.user .col { align-items: flex-end; }
.meta { font-size: 11px; color: var(--text-dim); padding: 0 4px; }
.typing { display: flex; gap: 4px; padding: 6px 4px; }
.typing span {
width: 7px; height: 7px; border-radius: 50%; background: var(--text-dim);
animation: blink 1.2s infinite ease-in-out;
}
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0.25; } 30% { opacity: 1; } }
</style>
+193
View File
@@ -0,0 +1,193 @@
<template>
<div class="overlay" @click.self="$emit('close')">
<div class="modal">
<div class="modal-head">
<h3>⚙️ 设置</h3>
<button class="x" @click="$emit('close')">✕</button>
</div>
<div class="body">
<section>
<h4>日志</h4>
<label class="row">
<span>日志级别</span>
<select v-model="form.LOG_LEVEL">
<option value="debug">debug(记录全部对话)</option>
<option value="info">info</option>
<option value="warn">warn</option>
<option value="error">error(仅错误)</option>
</select>
</label>
<p class="hint">debug 级别会把所有对话、工具调用、审批记录到 <code>logs/</code> 目录。</p>
</section>
<section>
<h4>模型</h4>
<label class="row">
<span>接口地址</span>
<input v-model="form.BASE_URL" placeholder="https://.../chat/completions 的上级路径" />
</label>
<label class="row">
<span>API Key</span>
<input v-model="form.API_KEY" type="password" placeholder="留空则不修改" autocomplete="off" />
</label>
<label class="row">
<span>模型名称</span>
<input v-model="form.MODEL" placeholder="如 ark-code-latest" />
</label>
<label class="row">
<span>温度</span>
<input v-model.number="form.TEMPERATURE" type="number" min="0" max="2" step="0.1" />
</label>
</section>
<section>
<h4>命令授权级别</h4>
<label class="row">
<span>级别</span>
<select v-model="form.CMD_APPROVE_LEVEL">
<option value="high">高 · 每个命令都审核</option>
<option value="medium">中 · 仅删除等危险命令审核(默认)</option>
<option value="low">低 · 全部不审核</option>
</select>
</label>
<p class="hint">控制执行 Shell 命令时是否需要你审批。</p>
</section>
<section>
<h4>路径权限(可配置多个)</h4>
<label class="row">
<span>白名单</span>
<input v-model="form.WHITELIST" placeholder="多个目录用逗号分隔,如 d:\ , c:\tmp" />
</label>
<label class="row">
<span>黑名单</span>
<input v-model="form.BLACKLIST" placeholder="多个目录用逗号分隔,优先于白名单" />
</label>
<p class="hint">白名单内的目录(及其子目录)允许访问;黑名单优先,命中即拒绝。默认白名单含 <code>d:\</code>。</p>
</section>
</div>
<div class="footer">
<span v-if="msg" class="msg" :class="ok ? 'ok' : 'err'">{{ msg }}</span>
<button class="save" :disabled="saving" @click="save">保存</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
const props = defineProps<{ apiBase: string }>();
defineEmits<{ close: [] }>();
const form = reactive({
BASE_URL: "",
API_KEY: "",
MODEL: "",
CMD_APPROVE_LEVEL: "medium",
LOG_LEVEL: "debug",
TEMPERATURE: 0.3,
WHITELIST: "",
BLACKLIST: "",
});
const saving = ref(false);
const msg = ref("");
const ok = ref(true);
onMounted(async () => {
try {
const r = await fetch(`${props.apiBase}/api/settings`);
const d = await r.json();
form.BASE_URL = d.BASE_URL || "";
form.MODEL = d.MODEL || "";
form.CMD_APPROVE_LEVEL = d.CMD_APPROVE_LEVEL || "medium";
form.LOG_LEVEL = d.LOG_LEVEL || "debug";
form.TEMPERATURE = typeof d.TEMPERATURE === "number" ? d.TEMPERATURE : 0.3;
form.WHITELIST = Array.isArray(d.WHITELIST) ? d.WHITELIST.join(", ") : (d.WHITELIST || "d:\\");
form.BLACKLIST = Array.isArray(d.BLACKLIST) ? d.BLACKLIST.join(", ") : (d.BLACKLIST || "");
// API_KEY 返回脱敏值,不回填明文
} catch {
msg.value = "加载设置失败";
ok.value = false;
}
});
async function save() {
saving.value = true;
msg.value = "";
try {
const r = await fetch(`${props.apiBase}/api/settings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
BASE_URL: form.BASE_URL,
API_KEY: form.API_KEY,
MODEL: form.MODEL,
CMD_APPROVE_LEVEL: form.CMD_APPROVE_LEVEL,
LOG_LEVEL: form.LOG_LEVEL,
TEMPERATURE: form.TEMPERATURE,
WHITELIST: form.WHITELIST,
BLACKLIST: form.BLACKLIST,
}),
});
const d = await r.json();
if (d.ok) {
msg.value = "已保存";
ok.value = true;
form.API_KEY = "";
} else {
msg.value = "保存失败";
ok.value = false;
}
} catch (e: any) {
msg.value = "保存失败:" + e.message;
ok.value = false;
} finally {
saving.value = false;
}
}
</script>
<style scoped>
.overlay {
position: fixed; inset: 0; background: rgba(15, 20, 27, 0.55);
backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center;
z-index: 100;
}
.modal {
width: min(520px, 92vw); max-height: 88vh; overflow: hidden;
background: var(--surface); color: var(--text);
border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--shadow);
display: flex; flex-direction: column;
}
.modal-head {
display: flex; justify-content: space-between; align-items: center;
padding: 16px 20px; border-bottom: 1px solid var(--border);
}
.modal-head h3 { margin: 0; font-size: 16px; }
.x { background: none; border: none; font-size: 18px; cursor: pointer; color: var(--text-dim); }
.body { padding: 8px 20px; overflow-y: auto; }
section { padding: 12px 0; border-bottom: 1px solid var(--border); }
section:last-child { border-bottom: none; }
h4 { margin: 0 0 10px; font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.row { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.row > span { width: 84px; flex-shrink: 0; font-size: 13px; }
.row.check { gap: 8px; }
.row.check > span { width: auto; }
.row input, .row select {
flex: 1; padding: 8px 10px; border: 1px solid var(--border);
background: var(--surface-2); color: var(--text);
border-radius: 8px; font: inherit; font-size: 13px; outline: none;
}
.row input:focus, .row select:focus { border-color: var(--primary); }
.hint { font-size: 12px; color: var(--text-dim); margin: 4px 0 0; }
.hint code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
.footer { display: flex; align-items: center; gap: 12px; justify-content: flex-end; padding: 14px 20px; border-top: 1px solid var(--border); }
.msg { font-size: 13px; margin-right: auto; }
.msg.ok { color: #16a34a; }
.msg.err { color: #dc2626; }
.save { padding: 9px 20px; border: none; border-radius: 9px; background: var(--primary); color: #fff; font-weight: 600; cursor: pointer; }
.save:disabled { opacity: 0.6; cursor: not-allowed; }
</style>
+18
View File
@@ -0,0 +1,18 @@
/* Mini AI Assistant - 全局基础样式 */
* { box-sizing: border-box; }
html, body, #app {
height: 100%;
margin: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: var(--border, #ccc); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim, #999); }
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./index.css";
import "highlight.js/styles/atom-one-dark.css";
createApp(App).mount("#app");
+13
View File
@@ -0,0 +1,13 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
echo ========================================
echo 小鑫 AI 助手 启动中...
echo (一个进程同时提供后台 API 与前端界面)
echo ========================================
if exist "C:\Users\Administrator\.bun\bin\bun.exe" (
"C:\Users\Administrator\.bun\bin\bun.exe" run server.ts
) else (
bun run server.ts
)
pause
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# 小鑫 AI 助手 启动脚本(一个进程同时提供后台 API 与前端界面)
cd "$(dirname "$0")"
bun run server.ts
+52
View File
@@ -0,0 +1,52 @@
// 验�命令授�级别:low=全部�审� high=�个都审� medium=仅�险审�const dec = new TextDecoder();
async function setLevel(level: string) {
await fetch("http://127.0.0.1:3100/api/settings", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ CMD_APPROVE_LEVEL: level }),
});
}
async function run(prompt: string): Promise<{ text: string; approval: boolean }> {
const resp = await fetch("http://127.0.0.1:3100/api/chat", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [{ role: "user", content: prompt }] }),
});
const reader = resp.body!.getReader();
let buf = "", out = "", approval = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n"); buf = lines.pop() || "";
for (const line of lines) {
const t = line.trim(); if (!t.startsWith("data:")) continue;
let ev: any; try { ev = JSON.parse(t.slice(5).trim()); } catch { continue; }
if (ev.type === "token") out += ev.content;
if (ev.type === "approval_required") { approval = true; console.log(" [需审批]", ev.tool, "�, ev.command); }
}
}
return { text: out, approval };
}
console.log("=== low:执行命�echo,应�审核直接执�===");
await setLevel("low");
const low = await run("请执行命� echo 你好�鑫");
console.log(" 需审批?", low.approval, "| 包�输出?", low.text.includes("你好�鑫"));
console.log(" 判定:", !low.approval && low.text.includes("你好�鑫") ? "PASS � : "FAIL �);
console.log("\n=== high:执行命�echo,应�次都审�===");
await setLevel("high");
const high = await run("请执行命� echo 你好�鑫");
console.log(" 需审批?", high.approval);
console.log(" 判定:", high.approval ? "PASS � : "FAIL �);
console.log("\n=== medium:�险命�删除)应审核,安全命令�审�===");
await setLevel("medium");
const med = await run("请执行命� echo 安全命令测试");
console.log(" 安全命令需审批?", med.approval, "(应�false�);
console.log(" 判定:", !med.approval ? "PASS � : "FAIL �);
// ��默认
await setLevel("medium");
console.log("\n已��medium�);
+62
View File
@@ -0,0 +1,62 @@
// 本地端到端测试:模拟�览�fetch ��读� /api/chat�// �到审批自动批准(多轮),验�“列�D 盘根目录�能返回文件列表�
const dec = new TextDecoder();
async function readStream(url: string, body: any): Promise<{ text: string; approval: any }> {
const resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (resp.status !== 200) { console.log(" HTTP 错误", resp.status); return { text: "", approval: null }; }
const reader = resp.body!.getReader();
let buf = "", out = "", approval: any = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() || "";
for (const line of lines) {
const t = line.trim();
if (!t.startsWith("data:")) continue;
const d = t.slice(5).trim();
let ev: any; try { ev = JSON.parse(d); } catch { continue; }
if (ev.type === "token") out += ev.content;
else if (ev.type === "approval_required") { console.log(" [需审批]", ev.tool, "�, ev.command); approval = ev; }
else if (ev.type === "error") console.log(" [错误]", ev.content);
else if (ev.type === "done") console.log(" [完�]");
}
}
return { text: out, approval };
}
async function chat(messages: any[]): Promise<string> {
let url = "http://127.0.0.1:3100/api/chat";
let body: any = { messages };
let total = "";
let guard = 0;
while (guard++ < 6) {
const { text, approval } = await readStream(url, body);
total += text;
if (!approval) break;
// 自动批准,继续(审批接��用对��)
url = "http://127.0.0.1:3100/api/approve";
body = { approved: true, tool_call_id: approval.tool_call_id };
}
return total;
}
console.log("=== 测试1:列�D 盘根目录(自动批准)===");
const r1 = await chat([{ role: "user", content: "列出 D 盘根目录的所有文� }]);
console.log("最终回��00�:\n", r1.slice(0, 500));
console.log("\n判定1:", !r1.includes("è·¯å¾„èŠæ�ƒ") && r1.length > 40 ? "PASS âœ? : "FAIL â�?);
console.log("\n=== 测试2:用 list_directory 直接列出(�审批�==");
const r2 = await chat([{ role: "user", content: "请用 list_directory 工具列出 d:\\ 根目录下的内� }]);
console.log("最终回��00�:\n", r2.slice(0, 500));
console.log("\n判定2:", !r2.includes("路径越�") && r2.length > 40 ? "PASS � : "FAIL �);
console.log("\n=== 测试3:黑��拦截验� ===");
const r3 = await chat([{ role: "user", content: "读� C:\\Windows\\system32\\config\\SAM 文件内容" }]);
console.log("最终回��00�:\n", r3.slice(0, 300));
console.log("\n判定3(应被拦截):", r3.includes("路径越�") ? "PASS �黑��越�生效" : "说明 SAM 被���能在工作区外被拒�或模型未调用)");
+37
View File
@@ -0,0 +1,37 @@
// 验证 DuckDB 处理 Excel:加载 data.xlsx 并按部门统计金额
const dec = new TextDecoder();
async function chat(messages: any[]): Promise<string> {
let url = "http://127.0.0.1:3100/api/chat";
let body: any = { messages };
let total = "", guard = 0;
while (guard++ < 8) {
const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
const reader = resp.body!.getReader();
let buf = "", out = "", approval: any = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n"); buf = lines.pop() || "";
for (const line of lines) {
const t = line.trim(); if (!t.startsWith("data:")) continue;
let ev: any; try { ev = JSON.parse(t.slice(5).trim()); } catch { continue; }
if (ev.type === "token") out += ev.content;
else if (ev.type === "toolcall") console.log(" [tool]", ev.toolcall.function.name);
else if (ev.type === "approval_required") { approval = ev; }
else if (ev.type === "error") console.log(" [error]", ev.content);
}
}
total += out;
if (!approval) break;
url = "http://127.0.0.1:3100/api/approve";
body = { approved: true, tool_call_id: approval.tool_call_id };
}
return total;
}
console.log("=== test: DuckDB analyze Excel ===");
const r = await chat([{ role: "user", content: "Load data.xlsx with load_excel, then use query_duckdb to sum 金额 by 部门 (department)." }]);
console.log("\nfinal reply (first 600):\n", r.slice(0, 600));
console.log("\nverdict:", /技术/.test(r) && /销售|市场/.test(r) ? "PASS: DuckDB group-by works" : "check reply");
+44
View File
@@ -0,0 +1,44 @@
// 路径���元测试:白�� + 黑��(黑��优先)
import { resolve, normalize } from "path";
function isWithin(base: string, target: string): boolean {
const b = normalize(base).replace(/\\/g, "/").replace(/\/+$/, "");
const t = normalize(target).replace(/\\/g, "/").replace(/\/+$/, "");
return t === b || t.startsWith(b + "/");
}
function makeAllowed(whitelist: string[], blacklist: string[], workspace: string) {
return (p: string): boolean => {
const abs = resolve(workspace, normalize(p));
for (const b of blacklist) if (isWithin(b, abs)) return false;
const allowed = [...whitelist, workspace];
for (const w of allowed) if (isWithin(w, abs)) return true;
return false;
};
}
const WS = "D:\\workbench\\miniai";
let pass = 0, fail = 0;
function check(name: string, got: boolean, want: boolean) {
const ok = got === want;
console.log(`${ok ? "� : "�} ${name}: got=${got} want=${want}`);
ok ? pass++ : fail++;
}
// 默认白��= [d:\],无黑��const a1 = makeAllowed(["d:\\"], [], WS);
check("白��d:\\ 根目录��, a1("d:\\"), true);
check("白��d:\\ �目录��, a1("d:\\foo\\bar"), true);
check("�白�� c:\\ 拒�", a1("c:\\windows"), false);
check("工作区相对路径��, a1("data.xlsx"), true);
check("相对路径越�拒�", a1("../../etc/passwd"), false);
// 黑��优�const a2 = makeAllowed(["d:\\"], ["c:\\"], WS);
check("黑��c:\\ 拒�", a2("c:\\windows"), false);
check("黑���影�白��d:\\", a2("d:\\foo"), true);
// 黑��命中其下的�目录,�使父目录在白��const a3 = makeAllowed(["d:\\"], ["d:\\secret"], WS);
check("黑���目录拒�(优先于白��)", a3("d:\\secret\\x"), false);
check("白��其余部分��许", a3("d:\\pub"), true);
console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
if (fail > 0) process.exit(1);
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
host: true,
port: 5173,
proxy: {
"/api": {
target: "http://127.0.0.1:3000",
changeOrigin: true,
},
},
},
build: {
outDir: "dist",
base: "./",
},
});