Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd3fed5b14 | |||
| b009f31aae | |||
| 4d7bcbe948 | |||
| 1b5bbdf467 | |||
| 6977f9fc24 | |||
| ce8247743e | |||
| 04859defeb | |||
| bacc31c83a | |||
| 693fc8a712 | |||
| 00a4855715 | |||
| 4f708d10bd | |||
| 65970f383d | |||
| f6624936d6 | |||
| c0d74b9486 | |||
| 75057bd285 | |||
| 6e874c7a8e | |||
| 790c5794e9 |
@@ -1 +0,0 @@
|
||||
26747
|
||||
@@ -1 +0,0 @@
|
||||
25645
|
||||
@@ -42,5 +42,10 @@ VITE_MAIN_APP_URL=https://h5.tkmind.cn
|
||||
MEMIND_UMAMI_SSO_SECRET=replace-with-the-same-random-secret-used-by-umami
|
||||
UMAMI_SSO_USERNAME=admin
|
||||
|
||||
# Rybbit 仅允许从 MemindAdm 单点登录进入(必须与 105 Rybbit 一致)
|
||||
MEMIND_RYBBIT_SSO_SECRET=replace-with-the-same-random-secret-used-by-rybbit
|
||||
RYBBIT_SSO_EMAIL=admin@tkmind.cn
|
||||
RYBBIT_URL=https://rybbit.tkmind.cn
|
||||
|
||||
# Plaza 帖子预览链接
|
||||
# VITE_PLAZA_BASE=https://plaza.tkmind.cn
|
||||
|
||||
@@ -8,4 +8,5 @@ dist-ssr/
|
||||
memind-lib/
|
||||
.vite/
|
||||
*.log
|
||||
*.pid
|
||||
logs/
|
||||
|
||||
Generated
+614
@@ -21,6 +21,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"redis": "^4.7.1",
|
||||
"sharp": "0.35.2",
|
||||
"undici": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -786,6 +787,554 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz",
|
||||
"integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz",
|
||||
"integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz",
|
||||
"integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz",
|
||||
"integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz",
|
||||
"integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz",
|
||||
"integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz",
|
||||
"integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz",
|
||||
"integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz",
|
||||
"integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz",
|
||||
"integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz",
|
||||
"integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz",
|
||||
"integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz",
|
||||
"integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz",
|
||||
"integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz",
|
||||
"integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz",
|
||||
"integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz",
|
||||
"integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz",
|
||||
"integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz",
|
||||
"integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz",
|
||||
"integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz",
|
||||
"integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz",
|
||||
"integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz",
|
||||
"integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz",
|
||||
"integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz",
|
||||
"integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz",
|
||||
"integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -2231,6 +2780,15 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
@@ -3710,6 +4268,62 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.2",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz",
|
||||
"integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.2",
|
||||
"@img/sharp-darwin-x64": "0.35.2",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.2",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.1",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.1",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.1",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.1",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.1",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.1",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.1",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.1",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.1",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.1",
|
||||
"@img/sharp-linux-arm": "0.35.2",
|
||||
"@img/sharp-linux-arm64": "0.35.2",
|
||||
"@img/sharp-linux-ppc64": "0.35.2",
|
||||
"@img/sharp-linux-riscv64": "0.35.2",
|
||||
"@img/sharp-linux-s390x": "0.35.2",
|
||||
"@img/sharp-linux-x64": "0.35.2",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.2",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.2",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.2",
|
||||
"@img/sharp-win32-arm64": "0.35.2",
|
||||
"@img/sharp-win32-ia32": "0.35.2",
|
||||
"@img/sharp-win32-x64": "0.35.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"redis": "^4.7.1",
|
||||
"sharp": "0.35.2",
|
||||
"undici": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+37
-3
@@ -85,14 +85,27 @@ need_cmd scp
|
||||
need_cmd rsync
|
||||
need_cmd tar
|
||||
need_cmd shasum
|
||||
need_cmd node
|
||||
|
||||
if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then
|
||||
bash "${ROOT}/scripts/check-release-ready.sh"
|
||||
ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh"
|
||||
else
|
||||
echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT_GIT_BRANCH="$(git -C "${ROOT}" branch --show-current)"
|
||||
ROOT_GIT_HEAD="$(git -C "${ROOT}" rev-parse HEAD)"
|
||||
ROOT_ORIGIN_HEAD="$(git -C "${ROOT}" rev-parse origin/main)"
|
||||
[[ "${ROOT_GIT_BRANCH}" == "main" ]] || {
|
||||
echo "memindadm 生产发布必须从 main 执行,当前分支: ${ROOT_GIT_BRANCH:-detached}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${ROOT_GIT_HEAD}" == "${ROOT_ORIGIN_HEAD}" ]] || {
|
||||
echo "memindadm main 必须与 origin/main 完全一致" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "${IGNORE_FILE}" ]] || {
|
||||
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
|
||||
exit 1
|
||||
@@ -110,6 +123,12 @@ if [[ "${SKIP_BUILD}" -ne 1 ]]; then
|
||||
)
|
||||
fi
|
||||
|
||||
say "验证共享模块图像运行依赖"
|
||||
(
|
||||
cd "${ROOT}"
|
||||
node --input-type=module -e "const { default: sharp } = await import('sharp'); await sharp({ create: { width: 1, height: 1, channels: 4, background: '#000000' } }).png().toBuffer();"
|
||||
)
|
||||
|
||||
[[ -d "${ROOT}/dist" ]] || {
|
||||
echo "缺少 dist/,请先本地构建或去掉 --skip-build" >&2
|
||||
exit 1
|
||||
@@ -120,8 +139,23 @@ fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current 2>/dev/null || echo detached)"
|
||||
git -C "${MEMIND_SRC}" fetch origin main --quiet
|
||||
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD)"
|
||||
MEMIND_ORIGIN_HEAD="$(git -C "${MEMIND_SRC}" rev-parse origin/main)"
|
||||
MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current)"
|
||||
[[ "${MEMIND_GIT_BRANCH}" == "main" ]] || {
|
||||
echo "Memind 共享模块必须从 main 打包,当前分支: ${MEMIND_GIT_BRANCH:-detached}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${MEMIND_GIT_HEAD}" == "${MEMIND_ORIGIN_HEAD}" ]] || {
|
||||
echo "Memind main 必须与 origin/main 完全一致" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -z "$(git -C "${MEMIND_SRC}" status --porcelain=v1 --untracked-files=all)" ]] || {
|
||||
echo "Memind 共享模块工作区必须干净" >&2
|
||||
git -C "${MEMIND_SRC}" status --short --untracked-files=all >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
say "打包 Memind 共享模块到 memind-lib"
|
||||
rm -rf "${MEMIND_LIB}"
|
||||
|
||||
@@ -34,6 +34,19 @@ export ADM_WEB_HOST="${WEB_HOST}"
|
||||
export ADM_PORT="${PORT}"
|
||||
export VITE_BASE_PATH="${BASE_PATH%/}"
|
||||
|
||||
wait_for_http() {
|
||||
local url="$1"
|
||||
local attempts="${2:-30}"
|
||||
local attempt
|
||||
for ((attempt = 1; attempt <= attempts; attempt += 1)); do
|
||||
if curl -sf --max-time 2 "${url}" >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ ! -d dist ]]; then
|
||||
echo "❌ ${ROOT}/dist 不存在,请先构建或 rsync dist" >&2
|
||||
exit 1
|
||||
@@ -61,9 +74,8 @@ sleep 1
|
||||
echo "==> 启动 Admin API @ ${API_HOST}:${API_PORT}..."
|
||||
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
echo $! >"${API_PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
|
||||
if ! wait_for_http "http://127.0.0.1:${API_PORT}/health" 30; then
|
||||
echo "❌ Admin API 启动失败,最近日志:" >&2
|
||||
tail -n 40 "${API_LOG}" >&2 || true
|
||||
exit 1
|
||||
@@ -72,11 +84,10 @@ fi
|
||||
echo "==> 启动 vite preview @ ${WEB_HOST}:${PORT} (base=${BASE_PATH})..."
|
||||
nohup npx vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 &
|
||||
echo $! >"${PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
preview_url="http://127.0.0.1:${PORT}${BASE_PATH}"
|
||||
login_probe="http://127.0.0.1:${PORT}/auth/status"
|
||||
if curl -sf "${preview_url}" >/dev/null && curl -sf "${login_probe}" >/dev/null; then
|
||||
if wait_for_http "${preview_url}" 30 && wait_for_http "${login_probe}" 15; then
|
||||
echo "✅ memind_adm 已就绪: ${preview_url}"
|
||||
curl -sf "${preview_url}" >/dev/null && echo " 首页 OK"
|
||||
echo " /auth 反代 OK"
|
||||
|
||||
@@ -232,6 +232,21 @@ export function createAdminApp(services) {
|
||||
res.json({ url: `${baseUrl}/auth/memind?ticket=${encoded}.${signature}` });
|
||||
});
|
||||
|
||||
adminApi.get('/analytics/rybbit-sso', requireAdmin, async (_req, res) => {
|
||||
const sharedSecret = process.env.MEMIND_RYBBIT_SSO_SECRET?.trim();
|
||||
if (!sharedSecret) return res.status(503).json({ message: '未配置 Rybbit 单点登录密钥' });
|
||||
const email = process.env.RYBBIT_SSO_EMAIL?.trim().toLowerCase();
|
||||
if (!email) return res.status(503).json({ message: '未配置 Rybbit 单点登录账号' });
|
||||
const baseUrl = String(process.env.RYBBIT_URL || 'https://rybbit.tkmind.cn').replace(/\/$/, '');
|
||||
const encoded = Buffer.from(JSON.stringify({
|
||||
email,
|
||||
exp: Math.floor(Date.now() / 1000) + 60,
|
||||
nonce: crypto.randomUUID(),
|
||||
})).toString('base64url');
|
||||
const signature = crypto.createHmac('sha256', sharedSecret).update(encoded).digest('base64url');
|
||||
res.json({ url: `${baseUrl}/api/auth/memind?ticket=${encoded}.${signature}` });
|
||||
});
|
||||
|
||||
adminApi.get('/users', requireAdmin, async (req, res) => {
|
||||
const result = await userAuth.listUsers({
|
||||
page: Number(req.query.page) || 1,
|
||||
@@ -401,6 +416,15 @@ export function createAdminApp(services) {
|
||||
if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.getRuntimeState());
|
||||
});
|
||||
adminApi.post('/mindsearch/services/:serviceId/test', requireAdmin, async (req, res) => {
|
||||
if (!mindSearchConfigService?.testService) return res.status(503).json({ message: 'MindSearch 服务测试未启用' });
|
||||
try {
|
||||
return res.json(await mindSearchConfigService.testService(req.params.serviceId));
|
||||
} catch (error) {
|
||||
if (error?.code === 'SEARCH_SERVICE_NOT_FOUND') return res.status(404).json({ message: error.message });
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
|
||||
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function bootstrapAdminServices() {
|
||||
const createLlmProviderService = await loadCreateLlmProviderService();
|
||||
const { createWechatAdminService } = await importMemind('wechat-admin.mjs');
|
||||
const { createWechatMpService, loadWechatMpConfig } = await importMemind('wechat-mp.mjs');
|
||||
const { createNotificationDispatcher } = await importMemind('notification-dispatcher.mjs');
|
||||
const { createPlazaPostService, formatPostRow } = await importMemind('plaza-posts.mjs');
|
||||
const { createPlazaInteractionService } = await importMemind('plaza-interactions.mjs');
|
||||
const { createPlazaOpsService } = await importMemind('plaza-ops.mjs');
|
||||
@@ -119,6 +120,14 @@ export async function bootstrapAdminServices() {
|
||||
throw new Error('admin api does not proxy WeChat chat sessions');
|
||||
},
|
||||
});
|
||||
const notificationDispatcher = createNotificationDispatcher({
|
||||
sendWechatTextToUser: wechatMpService?.enabled
|
||||
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
||||
: null,
|
||||
});
|
||||
userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => {
|
||||
await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey });
|
||||
});
|
||||
const wechatAdmin = createWechatAdminService(pool, {
|
||||
config: wechatMpConfig,
|
||||
scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
||||
import { getMindSpaceAdminConfig, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client';
|
||||
import { getMindSpaceAdminConfig, getRybbitSsoUrl, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client';
|
||||
|
||||
const initial = {
|
||||
enabled: false,
|
||||
@@ -16,7 +16,8 @@ export function AnalyticsConfigPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [opening, setOpening] = useState(false);
|
||||
const [openingUmami, setOpeningUmami] = useState(false);
|
||||
const [openingRybbit, setOpeningRybbit] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -52,23 +53,43 @@ export function AnalyticsConfigPage() {
|
||||
};
|
||||
|
||||
const openUmami = async () => {
|
||||
setOpening(true);
|
||||
setOpeningUmami(true);
|
||||
setError(null);
|
||||
try {
|
||||
window.location.assign(await getUmamiSsoUrl());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '无法打开 Umami');
|
||||
setOpening(false);
|
||||
setOpeningUmami(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openRybbit = async () => {
|
||||
setOpeningRybbit(true);
|
||||
setError(null);
|
||||
try {
|
||||
window.location.assign(await getRybbitSsoUrl());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '无法打开 Rybbit');
|
||||
setOpeningRybbit(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="admin-page">
|
||||
<div className="admin-page-head"><h2>Analytics 配置</h2><p className="muted">配置本地 Memind 生成页面的 Umami 统计。默认只连接本机 Umami,不涉及生产环境。</p></div>
|
||||
<div className="admin-page-head"><h2>Analytics 配置</h2><p className="muted">进入统计后台,或配置 Memind 生成页面的 Umami 统计。</p></div>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
<section className="admin-card">
|
||||
<h2>本地 Umami</h2>
|
||||
<div className="admin-actions" style={{ marginBottom: 16 }}><button type="button" className="send-btn" onClick={() => void openUmami()} disabled={opening || loading}>{opening ? '正在进入 Umami...' : '打开 Umami 分析后台'}</button></div>
|
||||
<h2>Rybbit 行为分析</h2>
|
||||
<p className="muted">查看页面访问、用户会话、停留时间、点击和会话回放等行为明细。</p>
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="send-btn" onClick={() => void openRybbit()} disabled={openingRybbit}>
|
||||
{openingRybbit ? '正在进入 Rybbit...' : '打开 Rybbit 分析后台'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="admin-card">
|
||||
<h2>Umami</h2>
|
||||
<div className="admin-actions" style={{ marginBottom: 16 }}><button type="button" className="send-btn" onClick={() => void openUmami()} disabled={openingUmami || loading}>{openingUmami ? '正在进入 Umami...' : '打开 Umami 分析后台'}</button></div>
|
||||
<form className="admin-form" onSubmit={save}>
|
||||
<label className="admin-form-row"><span>启用页面统计</span><input type="checkbox" checked={config.enabled} disabled={loading || busy} onChange={(e) => setConfig({ ...config, enabled: e.target.checked })} /></label>
|
||||
<label className="admin-form-row"><span>Website ID</span><input value={config.websiteId} disabled={loading || busy} onChange={(e) => setConfig({ ...config, websiteId: e.target.value })} placeholder="Umami Website ID" required={config.enabled} /></label>
|
||||
|
||||
@@ -12,6 +12,7 @@ type PluginDraft = {
|
||||
provider: string;
|
||||
llmProviderKeyId: string;
|
||||
llmModel: string;
|
||||
purposes: Record<string, boolean>;
|
||||
};
|
||||
|
||||
function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||||
@@ -20,6 +21,7 @@ function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||||
provider: plugin.provider ?? '',
|
||||
llmProviderKeyId: plugin.llmProviderKeyId ?? '',
|
||||
llmModel: plugin.llmModel ?? '',
|
||||
purposes: { ...(plugin.purposes ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +63,16 @@ export function AssetGatewayPage() {
|
||||
setDrafts((current) => ({ ...current, [pluginId]: { ...current[pluginId], ...patch } }));
|
||||
};
|
||||
|
||||
const updatePurpose = (pluginId: string, purposeId: string, enabled: boolean) => {
|
||||
setDrafts((current) => ({
|
||||
...current,
|
||||
[pluginId]: {
|
||||
...current[pluginId],
|
||||
purposes: { ...(current[pluginId]?.purposes ?? {}), [purposeId]: enabled },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const saveGlobal = async (enabled: boolean) => {
|
||||
setBusy('global'); setMessage(null); setError(null);
|
||||
try {
|
||||
@@ -81,6 +93,7 @@ export function AssetGatewayPage() {
|
||||
provider: draft.provider || null,
|
||||
llmProviderKeyId: draft.llmProviderKeyId || null,
|
||||
llmModel: draft.llmModel || null,
|
||||
purposes: draft.purposes,
|
||||
});
|
||||
setConfig(result.config);
|
||||
setDrafts(Object.fromEntries(result.config.plugins.map((item) => [item.pluginId, draftFromPlugin(item)])));
|
||||
@@ -126,11 +139,21 @@ export function AssetGatewayPage() {
|
||||
</label>
|
||||
</div>
|
||||
<div className="asset-plugin-fields">
|
||||
<label>运行 Provider<select value={draft.provider} onChange={(event) => updateDraft(plugin.pluginId, { provider: event.target.value })} disabled={!draft.enabled}>
|
||||
<label>运行 Provider<select value={draft.provider} onChange={(event) => {
|
||||
const provider = event.target.value;
|
||||
updateDraft(plugin.pluginId, {
|
||||
provider,
|
||||
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
|
||||
});
|
||||
}} disabled={!draft.enabled}>
|
||||
<option value="">选择 Provider</option>
|
||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select></label>
|
||||
{plugin.supportsLlm ? <>
|
||||
{plugin.pluginId === 'asset-generate' && draft.provider === 'image_make' ? (
|
||||
<div className="asset-image-make-note">
|
||||
image_make 独立管理本地模型,不读取统一模型中心密钥。
|
||||
</div>
|
||||
) : plugin.supportsLlm ? <>
|
||||
<label>专属 LLM<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
||||
<option value="">不使用 LLM</option>
|
||||
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
|
||||
@@ -140,6 +163,24 @@ export function AssetGatewayPage() {
|
||||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||
</select></label>
|
||||
</> : <div className="asset-no-llm">确定性处理 · 不调用 LLM</div>}
|
||||
{plugin.pluginId === 'asset-generate' && plugin.purposeCatalog?.length ? (
|
||||
<fieldset className="asset-purpose-fieldset" disabled={!draft.enabled || draft.provider !== 'image_make'}>
|
||||
<legend>image_make 生效范围</legend>
|
||||
<div className="asset-purpose-grid">
|
||||
{plugin.purposeCatalog.map((purpose) => (
|
||||
<label key={purpose.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(draft.purposes[purpose.id])}
|
||||
onChange={(event) => updatePurpose(plugin.pluginId, purpose.id, event.target.checked)}
|
||||
/>
|
||||
<span>{purpose.label}<small>{purpose.presetId}</small></span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p>信息流缩略图仍由 MindSpace 从封面源图派生,不会重复调用生图模型。</p>
|
||||
</fieldset>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="asset-plugin-footer"><span className={`asset-status ${draft.enabled ? 'is-on' : ''}`}>{draft.enabled ? '待保存为启用' : '默认安全关闭'}</span><button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存配置'}</button></div>
|
||||
</section>;
|
||||
|
||||
@@ -122,6 +122,12 @@ const CAPABILITIES: Array<{
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用组合召回', type: 'boolean' },
|
||||
{ key: 'episodicEnabled', label: '事件记忆', type: 'boolean' },
|
||||
{ key: 'episodicMode', label: '历史会话召回模式', type: 'select', options: [
|
||||
{ value: 'off', label: 'Off · 关闭' },
|
||||
{ value: 'canary', label: 'Canary · 仅指定用户' },
|
||||
{ value: 'active', label: 'Active · 全量' },
|
||||
] },
|
||||
{ key: 'episodicCanaryUserIds', label: '历史会话 Canary 用户 ID(逗号分隔)', type: 'text' },
|
||||
{ key: 'semanticEnabled', label: '语义记忆', type: 'boolean' },
|
||||
{ key: 'preferenceEnabled', label: '用户偏好', type: 'boolean' },
|
||||
{ key: 'goalEnabled', label: '活跃目标', type: 'boolean' },
|
||||
@@ -346,7 +352,7 @@ function defaultConfig(): MemoryV2AdminConfig {
|
||||
candidateMemory: { enabled: false, mode: 'active', minImportance: '0.7', minConfidence: '0.8', maxPending: '500', persistenceEnabled: false },
|
||||
runtimeControl: { agentResolveEnabled: false, agentInjectionMode: 'off', agentCanaryUserIds: '', agentResolveLimit: '3', agentResolveTimeoutMs: '1200', promotionEnabled: false, compactionV2Enabled: false, reflectionEnabled: false, lifecycleWorkerEnabled: false, lifecycleRolloutMode: 'off', lifecycleRolloutUserIds: '' },
|
||||
policy: { enabled: false, saveExplicit: true, rejectSensitive: true, requireEvidence: true, retentionDays: '365' },
|
||||
retriever: { enabled: false, episodicEnabled: true, semanticEnabled: true, preferenceEnabled: true, goalEnabled: true, limit: '12', tokenBudget: '1800', timeoutMs: '1200' },
|
||||
retriever: { enabled: false, episodicEnabled: true, episodicMode: 'off', episodicCanaryUserIds: '', semanticEnabled: true, preferenceEnabled: true, goalEnabled: true, limit: '12', tokenBudget: '1800', timeoutMs: '1200' },
|
||||
lifecycle: { enabled: false, dedupeEnabled: true, conflictReview: true, decayEnabled: false, forgettingEnabled: true, compactIntervalHours: '24' },
|
||||
persona: { enabled: false, provider: 'none', shadowMode: true, maxTokens: '400', cacheTtlSeconds: '300' },
|
||||
graph: { enabled: false, provider: 'postgres', maxDepth: '2', relationLimit: '20' },
|
||||
|
||||
@@ -1,23 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMindSearchConfig, updateMindSearchConfig } from '../../api/client';
|
||||
import type { MindSearchConfig } from '../../types';
|
||||
import {
|
||||
getMindSearchConfig,
|
||||
listLlmProviderKeys,
|
||||
testLlmProviderKey,
|
||||
testMindSearchService,
|
||||
updateMindSearchConfig,
|
||||
} from '../../api/client';
|
||||
import type {
|
||||
LlmProviderKeyRow,
|
||||
MindSearchConfig,
|
||||
MindSearchRoutes,
|
||||
MindSearchService,
|
||||
} from '../../types';
|
||||
|
||||
const builtInServices: MindSearchService[] = [
|
||||
{ id: 'searxng', name: 'SearXNG', kind: 'provider', adapter: 'searxng', capabilities: ['search.web', 'search.news'], endpoint: '', healthPath: '/config', enabled: false, timeoutMs: 8000, priority: 100 },
|
||||
{ id: 'github', name: 'GitHub Code', kind: 'provider', adapter: 'github', capabilities: ['search.code'], endpoint: 'https://api.github.com/search/code', healthPath: '', enabled: false, timeoutMs: 8000, priority: 90 },
|
||||
{ id: 'reader', name: 'Safe Reader', kind: 'reader', adapter: 'reader', capabilities: ['search.read'], endpoint: '', healthPath: '', enabled: false, timeoutMs: 8000, priority: 80 },
|
||||
{ id: 'deep-search', name: 'TKMind Deep Search', kind: 'orchestrator', adapter: 'research-http', capabilities: ['search.web', 'research.plan', 'research.execute'], endpoint: 'http://127.0.0.1:20100', healthPath: '/health', enabled: false, timeoutMs: 120000, priority: 100, llmProviderKeyId: '', llmModel: '' },
|
||||
];
|
||||
|
||||
const initial: MindSearchConfig = {
|
||||
enabled: false,
|
||||
mode: 'off',
|
||||
providers: { searxng: false, github: false, reader: false },
|
||||
settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 },
|
||||
services: builtInServices,
|
||||
routes: { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: 'deep-search' },
|
||||
};
|
||||
|
||||
const routeLabels: Record<keyof MindSearchRoutes, string> = {
|
||||
web: '普通网页搜索',
|
||||
news: '新闻搜索',
|
||||
code: '代码搜索',
|
||||
read: '网页读取',
|
||||
research: '深度研究',
|
||||
};
|
||||
|
||||
function mergeConfig(config: MindSearchConfig): MindSearchConfig {
|
||||
const configuredServices = config.services ?? [];
|
||||
const configuredServiceIds = new Set(configuredServices.map((service) => service.id));
|
||||
const services = configuredServices.length
|
||||
? [
|
||||
...configuredServices,
|
||||
...builtInServices.filter((service) => !configuredServiceIds.has(service.id)),
|
||||
]
|
||||
: builtInServices;
|
||||
|
||||
return {
|
||||
...initial,
|
||||
...config,
|
||||
settings: { ...initial.settings, ...config.settings },
|
||||
services,
|
||||
routes: { ...initial.routes, ...config.routes },
|
||||
};
|
||||
}
|
||||
|
||||
export function MindSearchPage() {
|
||||
const [config, setConfig] = useState(initial);
|
||||
const [llmKeys, setLlmKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||
const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
|
||||
const [message, setMessage] = useState('加载配置中…');
|
||||
|
||||
useEffect(() => {
|
||||
getMindSearchConfig()
|
||||
.then((result) => {
|
||||
setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } });
|
||||
Promise.all([
|
||||
getMindSearchConfig(),
|
||||
listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]),
|
||||
])
|
||||
.then(([result, keys]) => {
|
||||
setConfig(mergeConfig(result.config));
|
||||
setLlmKeys(keys.filter((key) => key.status === 'active'));
|
||||
setMeta(result);
|
||||
setMessage('');
|
||||
})
|
||||
@@ -28,7 +80,7 @@ export function MindSearchPage() {
|
||||
setMessage('保存中…');
|
||||
try {
|
||||
const result = await updateMindSearchConfig(config);
|
||||
setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } });
|
||||
setConfig(mergeConfig(result.config));
|
||||
setMeta(result);
|
||||
setMessage('已保存到数据库;新会话生效,旧会话不受影响');
|
||||
} catch (error) {
|
||||
@@ -36,30 +88,207 @@ export function MindSearchPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProvider = (key: keyof MindSearchConfig['providers']) =>
|
||||
setConfig((current) => ({ ...current, providers: { ...current.providers, [key]: !current.providers[key] } }));
|
||||
const updateSetting = (key: keyof MindSearchConfig['settings'], value: string | number) =>
|
||||
setConfig((current) => ({ ...current, settings: { ...current.settings, [key]: value } }));
|
||||
const updateService = (id: string, patch: Partial<MindSearchService>) =>
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
services: current.services.map((service) => service.id === id ? { ...service, ...patch } : service),
|
||||
}));
|
||||
const addService = () => {
|
||||
const existingIds = new Set(config.services.map((service) => service.id));
|
||||
let suffix = config.services.length + 1;
|
||||
while (existingIds.has(`research-${suffix}`)) suffix += 1;
|
||||
const id = `research-${suffix}`;
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
services: [...current.services, {
|
||||
id,
|
||||
name: 'Research Engine',
|
||||
kind: 'orchestrator',
|
||||
adapter: 'research-http',
|
||||
capabilities: ['search.web', 'research.plan', 'research.execute'],
|
||||
endpoint: 'http://127.0.0.1:20100',
|
||||
healthPath: '/health',
|
||||
enabled: false,
|
||||
timeoutMs: 30000,
|
||||
priority: 100,
|
||||
llmProviderKeyId: '',
|
||||
llmModel: '',
|
||||
}],
|
||||
}));
|
||||
};
|
||||
const removeService = (id: string) =>
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
services: current.services.filter((service) => service.id !== id),
|
||||
routes: Object.fromEntries(
|
||||
Object.entries(current.routes).map(([key, value]) => [key, value === id ? '' : value]),
|
||||
) as unknown as MindSearchRoutes,
|
||||
}));
|
||||
const runServiceTest = async (id: string) => {
|
||||
setMessage(`测试 ${id} 中…`);
|
||||
try {
|
||||
const result = await testMindSearchService(id);
|
||||
setMessage(`${result.ok ? '通过' : '失败'}:${id};${result.message};${result.latencyMs}ms${result.resultCount == null ? '' : `;${result.resultCount} 条结果`}`);
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : '服务测试失败');
|
||||
}
|
||||
};
|
||||
const runLlmTest = async (service: MindSearchService) => {
|
||||
if (!service.llmProviderKeyId || !service.llmModel) {
|
||||
setMessage('请先选择 Deep Search 的 LLM 配置和模型');
|
||||
return;
|
||||
}
|
||||
setMessage(`测试 ${service.llmModel} 中…`);
|
||||
try {
|
||||
const result = await testLlmProviderKey(service.llmProviderKeyId, service.llmModel);
|
||||
setMessage(result.ok
|
||||
? `LLM 通过:${result.model ?? service.llmModel}${result.latencyMs == null ? '' : `;${result.latencyMs}ms`}`
|
||||
: `LLM 失败:${result.message ?? '模型连接失败'}`);
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : 'LLM 测试失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-header"><div><h1>MindSearch</h1><p>可插拔外部搜索增强。关闭时完全保持原有搜索、记忆和上下文链路。</p></div></div>
|
||||
<div className="admin-page-header">
|
||||
<div>
|
||||
<h1>MindSearch</h1>
|
||||
<p>可插拔搜索与研究服务。关闭时保持原有搜索、记忆和上下文链路。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-card">
|
||||
<h2>总开关与模式</h2>
|
||||
<label><input type="checkbox" checked={config.enabled} onChange={(event) => setConfig({ ...config, enabled: event.target.checked, mode: event.target.checked ? (config.mode === 'off' ? 'shadow' : config.mode) : 'off' })} /> 启用 MindSearch</label>
|
||||
<label>运行模式 <select value={config.mode} onChange={(event) => setConfig({ ...config, mode: event.target.value as MindSearchConfig['mode'] })}><option value="off">关闭</option><option value="shadow">Shadow(只记录,不注入)</option><option value="assist">Assist(补充工具)</option></select></label>
|
||||
<h2>Provider</h2>
|
||||
<fieldset><legend>可用 Provider</legend>
|
||||
<label><input type="checkbox" checked={config.providers.searxng} onChange={() => toggleProvider('searxng')} /> SearXNG Web/News</label>
|
||||
<label><input type="checkbox" checked={config.providers.github} onChange={() => toggleProvider('github')} /> GitHub Code(Token 通过环境变量配置,不在页面回显)</label>
|
||||
<label><input type="checkbox" checked={config.providers.reader} onChange={() => toggleProvider('reader')} /> Reader(阻止 localhost/内网地址)</label>
|
||||
</fieldset>
|
||||
<div className="admin-form">
|
||||
<label className="search-service-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
onChange={(event) => setConfig({
|
||||
...config,
|
||||
enabled: event.target.checked,
|
||||
mode: event.target.checked ? (config.mode === 'off' ? 'shadow' : config.mode) : 'off',
|
||||
})}
|
||||
/>
|
||||
<span>启用 MindSearch</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>运行模式</span>
|
||||
<select value={config.mode} onChange={(event) => setConfig({ ...config, mode: event.target.value as MindSearchConfig['mode'] })}>
|
||||
<option value="off">关闭</option>
|
||||
<option value="shadow">Shadow(只记录,不注入)</option>
|
||||
<option value="assist">Assist(补充工具)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>服务注册表</h2>
|
||||
<p>服务独立部署;后台只管理连接、路由和健康检查,不直接操作 Docker 或生产进程。测试使用已保存配置。</p>
|
||||
<div className="search-service-grid">
|
||||
{config.services.map((service) => {
|
||||
const selectedLlmKey = llmKeys.find((key) => key.id === service.llmProviderKeyId);
|
||||
return (
|
||||
<fieldset key={service.id} className="search-service-card">
|
||||
<legend>{service.name}({service.id})</legend>
|
||||
<label className="search-service-toggle"><input type="checkbox" checked={service.enabled} onChange={(event) => updateService(service.id, { enabled: event.target.checked })} /><span>启用服务</span></label>
|
||||
<div className="admin-form">
|
||||
<label><span>名称</span><input value={service.name} onChange={(event) => updateService(service.id, { name: event.target.value })} /></label>
|
||||
<label>
|
||||
<span>类型</span>
|
||||
<select value={service.kind} onChange={(event) => updateService(service.id, { kind: event.target.value as MindSearchService['kind'] })}>
|
||||
<option value="provider">Provider</option>
|
||||
<option value="reader">Reader</option>
|
||||
<option value="orchestrator">Orchestrator</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>适配器</span>
|
||||
<select value={service.adapter} onChange={(event) => updateService(service.id, { adapter: event.target.value as MindSearchService['adapter'] })}>
|
||||
<option value="searxng">SearXNG</option>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="reader">Reader</option>
|
||||
<option value="research-http">Research HTTP</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>健康检查路径</span><input value={service.healthPath} placeholder="/health" onChange={(event) => updateService(service.id, { healthPath: event.target.value })} /></label>
|
||||
<label className="form-span-all"><span>Endpoint</span><input value={service.endpoint} placeholder="http://127.0.0.1:20100" onChange={(event) => updateService(service.id, { endpoint: event.target.value })} /></label>
|
||||
<label className="form-span-all"><span>Capabilities</span><input value={service.capabilities.join(', ')} onChange={(event) => updateService(service.id, { capabilities: event.target.value.split(',').map((item) => item.trim()).filter(Boolean) })} /></label>
|
||||
<label><span>超时(毫秒)</span><input type="number" min={1000} max={120000} value={service.timeoutMs} onChange={(event) => updateService(service.id, { timeoutMs: Number(event.target.value) })} /></label>
|
||||
<label><span>优先级</span><input type="number" min={1} max={1000} value={service.priority} onChange={(event) => updateService(service.id, { priority: Number(event.target.value) })} /></label>
|
||||
{service.adapter === 'research-http' && <>
|
||||
<label className="form-span-all">
|
||||
<span>规划与报告 LLM(可选)</span>
|
||||
<select
|
||||
value={service.llmProviderKeyId ?? ''}
|
||||
onChange={(event) => {
|
||||
const key = llmKeys.find((item) => item.id === event.target.value);
|
||||
updateService(service.id, {
|
||||
llmProviderKeyId: event.target.value,
|
||||
llmModel: key?.defaultModel ?? '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">不使用 LLM(确定性降级)</option>
|
||||
{service.llmProviderKeyId && !selectedLlmKey
|
||||
&& <option value={service.llmProviderKeyId}>原 LLM 配置已停用或删除</option>}
|
||||
{llmKeys.map((key) => (
|
||||
<option key={key.id} value={key.id}>{key.name} · {key.providerLabel}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="form-span-all">
|
||||
<span>模型</span>
|
||||
<select
|
||||
value={service.llmModel ?? ''}
|
||||
disabled={!selectedLlmKey}
|
||||
onChange={(event) => updateService(service.id, { llmModel: event.target.value })}
|
||||
>
|
||||
{!selectedLlmKey && <option value="">请先选择 LLM 配置</option>}
|
||||
{selectedLlmKey && service.llmModel && !selectedLlmKey.models.includes(service.llmModel)
|
||||
&& <option value={service.llmModel}>原模型已不可用:{service.llmModel}</option>}
|
||||
{selectedLlmKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<p className="search-llm-hint form-span-all">密钥复用“统一模型中心”,不会保存到 MindSearch 配置或下发给 Deep Search 服务。</p>
|
||||
</>}
|
||||
</div>
|
||||
<div className="search-service-actions">
|
||||
<button className="send-btn" type="button" onClick={() => runServiceTest(service.id)}>测试已保存配置</button>
|
||||
{service.adapter === 'research-http' && service.llmProviderKeyId && service.llmModel
|
||||
&& <button type="button" onClick={() => runLlmTest(service)}>测试 LLM</button>}
|
||||
{!['searxng', 'github', 'reader', 'deep-search'].includes(service.id) && <button type="button" onClick={() => removeService(service.id)}>移除</button>}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button className="send-btn" type="button" onClick={addService}>添加 Research Service</button>
|
||||
|
||||
<h2>能力路由</h2>
|
||||
<div className="admin-form">
|
||||
{(Object.keys(routeLabels) as Array<keyof MindSearchRoutes>).map((route) => (
|
||||
<label key={route}>
|
||||
<span>{routeLabels[route]}</span>
|
||||
<select
|
||||
value={config.routes[route]}
|
||||
onChange={(event) => setConfig((current) => ({ ...current, routes: { ...current.routes, [route]: event.target.value } }))}
|
||||
>
|
||||
<option value="">未配置</option>
|
||||
{config.services.map((service) => <option key={service.id} value={service.id}>{service.name}({service.id})</option>)}
|
||||
</select>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2>运行参数</h2>
|
||||
<label>SearXNG 地址 <input value={config.settings.searxngEndpoint} placeholder="http://127.0.0.1:8080/search" onChange={(event) => updateSetting('searxngEndpoint', event.target.value)} /></label>
|
||||
<label>最大结果数 <input type="number" min={1} max={20} value={config.settings.maxResults} onChange={(event) => updateSetting('maxResults', Number(event.target.value))} /></label>
|
||||
<label>Provider 超时(毫秒) <input type="number" min={1000} max={30000} value={config.settings.timeoutMs} onChange={(event) => updateSetting('timeoutMs', Number(event.target.value))} /></label>
|
||||
<label>Reader 最大字符数 <input type="number" min={1000} max={50000} value={config.settings.readerMaxChars} onChange={(event) => updateSetting('readerMaxChars', Number(event.target.value))} /></label>
|
||||
<button type="button" onClick={save}>保存配置</button>
|
||||
<div className="admin-form">
|
||||
<label><span>最大结果数</span><input type="number" min={1} max={20} value={config.settings.maxResults} onChange={(event) => updateSetting('maxResults', Number(event.target.value))} /></label>
|
||||
<label><span>默认 Provider 超时(毫秒)</span><input type="number" min={1000} max={30000} value={config.settings.timeoutMs} onChange={(event) => updateSetting('timeoutMs', Number(event.target.value))} /></label>
|
||||
<label><span>Reader 最大字符数</span><input type="number" min={1000} max={50000} value={config.settings.readerMaxChars} onChange={(event) => updateSetting('readerMaxChars', Number(event.target.value))} /></label>
|
||||
</div>
|
||||
<button className="send-btn" type="button" onClick={save}>保存配置</button>
|
||||
{message && <p role="status">{message}</p>}
|
||||
<p>配置来源:{meta.source === 'admin' ? '数据库' : '环境变量默认值'};最后更新:{meta.updatedAt ? new Date(meta.updatedAt).toLocaleString() : '尚未保存'}{meta.updatedBy ? `;操作人:${meta.updatedBy}` : ''}</p>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,7 @@ import type {
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
MindSearchConfig,
|
||||
MindSearchServiceTestResult,
|
||||
} from '../types';
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -307,6 +308,7 @@ export async function updateAssetPluginConfig(
|
||||
provider?: string | null;
|
||||
llmProviderKeyId?: string | null;
|
||||
llmModel?: string | null;
|
||||
purposes?: Record<string, boolean>;
|
||||
},
|
||||
): Promise<{ ok: boolean; config: AssetGatewayConfig }> {
|
||||
return portalFetch(`/admin-api/asset-gateway/plugins/${encodeURIComponent(pluginId)}`, {
|
||||
@@ -325,6 +327,11 @@ export async function getUmamiSsoUrl(): Promise<string> {
|
||||
return result.url;
|
||||
}
|
||||
|
||||
export async function getRybbitSsoUrl(): Promise<string> {
|
||||
const result = await portalFetch<{ url: string }>('/admin-api/analytics/rybbit-sso');
|
||||
return result.url;
|
||||
}
|
||||
|
||||
export async function updateMindSpaceAdminConfig(
|
||||
payload: Partial<MindSpaceAdminConfig> & { analytics?: MindSpaceAnalyticsUpdate },
|
||||
): Promise<MindSpaceAdminConfig> {
|
||||
@@ -668,6 +675,10 @@ export async function updateMindSearchConfig(config: Partial<MindSearchConfig>):
|
||||
return portalFetch('/admin-api/mindsearch/config', { method: 'PATCH', body: JSON.stringify(config) });
|
||||
}
|
||||
|
||||
export async function testMindSearchService(serviceId: string): Promise<MindSearchServiceTestResult> {
|
||||
return portalFetch(`/admin-api/mindsearch/services/${encodeURIComponent(serviceId)}/test`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// ── Policies ──────────────────────────────────────────
|
||||
|
||||
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
|
||||
|
||||
@@ -577,6 +577,51 @@ body,
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.search-service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
|
||||
gap: 14px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.search-service-card {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--color-border-input);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.search-service-card legend {
|
||||
padding: 0 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-service-toggle {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
.search-service-toggle input {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-service-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.search-llm-hint {
|
||||
margin: -2px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.form-span-all {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -1080,6 +1125,60 @@ body,
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-image-make-note,
|
||||
.asset-no-llm {
|
||||
padding: 9px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(132, 110, 255, .1);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.asset-purpose-fieldset {
|
||||
min-width: 0;
|
||||
margin: 2px 0 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.asset-purpose-fieldset legend {
|
||||
padding: 0 5px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-purpose-fieldset > p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.asset-purpose-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.asset-purpose-grid label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
padding: 7px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: rgba(255, 255, 255, .035);
|
||||
}
|
||||
|
||||
.asset-purpose-grid small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 9px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -208,6 +208,12 @@ export type AssetPluginConfig = {
|
||||
llmProviderId: string | null;
|
||||
llmModel: string | null;
|
||||
updatedAt: number | null;
|
||||
purposes?: Record<string, boolean>;
|
||||
purposeCatalog?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
presetId: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type AssetGatewayConfig = {
|
||||
@@ -619,4 +625,39 @@ export type MindSearchConfig = {
|
||||
mode: 'off' | 'shadow' | 'assist';
|
||||
providers: { searxng: boolean; github: boolean; reader: boolean };
|
||||
settings: { searxngEndpoint: string; maxResults: number; timeoutMs: number; readerMaxChars: number };
|
||||
services: MindSearchService[];
|
||||
routes: MindSearchRoutes;
|
||||
};
|
||||
|
||||
export type MindSearchService = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: 'provider' | 'reader' | 'orchestrator';
|
||||
adapter: 'searxng' | 'github' | 'reader' | 'research-http';
|
||||
capabilities: string[];
|
||||
endpoint: string;
|
||||
healthPath: string;
|
||||
enabled: boolean;
|
||||
timeoutMs: number;
|
||||
priority: number;
|
||||
llmProviderKeyId?: string;
|
||||
llmModel?: string;
|
||||
};
|
||||
|
||||
export type MindSearchRoutes = {
|
||||
web: string;
|
||||
news: string;
|
||||
code: string;
|
||||
read: string;
|
||||
research: string;
|
||||
};
|
||||
|
||||
export type MindSearchServiceTestResult = {
|
||||
ok: boolean;
|
||||
serviceId: string;
|
||||
adapter: MindSearchService['adapter'];
|
||||
status: number | null;
|
||||
latencyMs: number;
|
||||
resultCount?: number | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user