Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0d74b9486 | |||
| 75057bd285 | |||
| 6e874c7a8e | |||
| 75b7c07a4d | |||
| d864bcd6fb | |||
| 7c2af3306a | |||
| 23a5f147fd | |||
| 05681d758e | |||
| 7df3ae35f1 | |||
| 731d93843d | |||
| 4cbacb8529 | |||
| a019da24b0 | |||
| 6bd0faac40 | |||
| 0358484942 | |||
| 90cbcbba7c | |||
| a8a130c08f | |||
| 4b4bc898bd |
@@ -38,5 +38,9 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users
|
||||
# 超管页「返回对话」跳转主 H5(可选)
|
||||
VITE_MAIN_APP_URL=https://h5.tkmind.cn
|
||||
|
||||
# Umami 仅允许从 MemindAdm 单点登录进入(必须与 memind-analytics 一致)
|
||||
MEMIND_UMAMI_SSO_SECRET=replace-with-the-same-random-secret-used-by-umami
|
||||
UMAMI_SSO_USERNAME=admin
|
||||
|
||||
# Plaza 帖子预览链接
|
||||
# VITE_PLAZA_BASE=https://plaza.tkmind.cn
|
||||
|
||||
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": {
|
||||
|
||||
@@ -85,6 +85,7 @@ 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"
|
||||
@@ -110,6 +111,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,6 +127,9 @@ 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)"
|
||||
|
||||
say "打包 Memind 共享模块到 memind-lib"
|
||||
rm -rf "${MEMIND_LIB}"
|
||||
mkdir -p "${MEMIND_LIB}"
|
||||
@@ -133,10 +143,32 @@ rsync -a \
|
||||
--include '*.js' \
|
||||
--exclude '*' \
|
||||
"${MEMIND_SRC}/" "${MEMIND_LIB}/"
|
||||
|
||||
if [[ -d "${MEMIND_SRC}/skills" ]]; then
|
||||
say "打包 Memind 平台技能目录到 memind-lib"
|
||||
mkdir -p "${MEMIND_LIB}/skills"
|
||||
rsync -a \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude '__pycache__/' \
|
||||
"${MEMIND_SRC}/skills/" "${MEMIND_LIB}/skills/"
|
||||
fi
|
||||
|
||||
[[ -f "${MEMIND_LIB}/user-auth.mjs" ]] || {
|
||||
echo "memind-lib 缺少 user-auth.mjs" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "${MEMIND_LIB}/skills/excel-analyst/SKILL.md" ]] || {
|
||||
echo "memind-lib 缺少 Excel Analyst 技能" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q 'EXCEL_ANALYST_SKILL_NAME' "${MEMIND_LIB}/skills-registry.mjs" || {
|
||||
echo "memind-lib 缺少 Excel Analyst 技能授权映射" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q 'tkmind-excel' "${MEMIND_LIB}/capabilities.mjs" || {
|
||||
echo "memind-lib 缺少 Excel Analyst runtime 扩展映射" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
|
||||
say "发布确认"
|
||||
@@ -158,6 +190,8 @@ say "生成发布清单"
|
||||
echo "root=${ROOT}"
|
||||
echo "git_head=$(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
|
||||
echo "memind_git_head=${MEMIND_GIT_HEAD}"
|
||||
echo "memind_git_branch=${MEMIND_GIT_BRANCH}"
|
||||
echo "git_status_begin"
|
||||
git -C "${ROOT}" status --short --branch || true
|
||||
echo "git_status_end"
|
||||
|
||||
+169
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -91,12 +92,16 @@ export function createAdminApp(services) {
|
||||
const {
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
pool,
|
||||
ready,
|
||||
wechatAdmin,
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -216,6 +221,17 @@ export function createAdminApp(services) {
|
||||
next();
|
||||
}
|
||||
|
||||
adminApi.get('/analytics/sso', requireAdmin, async (req, res) => {
|
||||
const sharedSecret = process.env.MEMIND_UMAMI_SSO_SECRET?.trim();
|
||||
if (!sharedSecret) return res.status(503).json({ message: '未配置 Umami 单点登录密钥' });
|
||||
const config = loadMindSpaceConfig ? await loadMindSpaceConfig(pool) : null;
|
||||
const baseUrl = String(config?.analytics?.analyticsUrl || process.env.UMAMI_URL || 'http://127.0.0.1:3100').replace(/\/$/, '');
|
||||
const username = process.env.UMAMI_SSO_USERNAME?.trim() || 'admin';
|
||||
const encoded = Buffer.from(JSON.stringify({ username, 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}/auth/memind?ticket=${encoded}.${signature}` });
|
||||
});
|
||||
|
||||
adminApi.get('/users', requireAdmin, async (req, res) => {
|
||||
const result = await userAuth.listUsers({
|
||||
page: Number(req.query.page) || 1,
|
||||
@@ -321,10 +337,38 @@ export function createAdminApp(services) {
|
||||
if (!updateMindSpaceConfig) return res.status(503).json({ message: 'MindSpace 配置未启用' });
|
||||
const result = await updateMindSpaceConfig(pool, {
|
||||
publicPageLimit: req.body?.publicPageLimit,
|
||||
analytics: req.body?.analytics,
|
||||
});
|
||||
res.json({ config: result });
|
||||
});
|
||||
|
||||
adminApi.get('/asset-gateway/config', requireAdmin, async (_req, res) => {
|
||||
if (!assetGatewayConfigService?.getConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
res.json(await assetGatewayConfigService.getConfig());
|
||||
});
|
||||
|
||||
adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => {
|
||||
if (!assetGatewayConfigService?.updateGlobalConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
res.json(await assetGatewayConfigService.updateGlobalConfig(req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
}));
|
||||
});
|
||||
|
||||
adminApi.put('/asset-gateway/plugins/:pluginId', requireAdmin, async (req, res) => {
|
||||
if (!assetGatewayConfigService?.updatePluginConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
const result = await assetGatewayConfigService.updatePluginConfig(req.params.pluginId, req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.ok) return res.status(400).json({ message: result.message });
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
||||
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
||||
res.json(await memoryV2ConfigService.getAdminConfig());
|
||||
@@ -338,6 +382,131 @@ export function createAdminApp(services) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/mindsearch/config', requireAdmin, async (_req, res) => {
|
||||
if (!mindSearchConfigService?.getAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
adminApi.patch('/mindsearch/config', requireAdmin, async (req, res) => {
|
||||
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
||||
});
|
||||
|
||||
adminApi.put('/mindsearch/config', requireAdmin, async (req, res) => {
|
||||
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
||||
});
|
||||
|
||||
adminApi.get('/mindsearch/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.getRuntimeState());
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
|
||||
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||
try {
|
||||
const response = await fetch(`${portalBaseUrl}/api/runtime/status`, {
|
||||
headers: { accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return res.status(502).json({
|
||||
ok: false,
|
||||
message: `Memory V2 运行时状态返回 HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
const payload = await response.json();
|
||||
return res.json({
|
||||
ok: true,
|
||||
checkedAt: Date.now(),
|
||||
memory: payload?.memory ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(503).json({
|
||||
ok: false,
|
||||
checkedAt: Date.now(),
|
||||
message: err instanceof Error ? err.message : 'Memory V2 运行时不可用',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/candidates', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.listCandidates) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
try {
|
||||
const status = String(req.query.status ?? 'candidate');
|
||||
const userId = String(req.query.userId ?? '').trim() || null;
|
||||
const limit = Number(req.query.limit ?? 50);
|
||||
const [items, counts] = await Promise.all([
|
||||
personalMemoryCandidateStore.listCandidates({ status, userId, limit }),
|
||||
personalMemoryCandidateStore.countByStatus(),
|
||||
]);
|
||||
return res.json({ items, counts });
|
||||
} catch (err) {
|
||||
const missingTable = err?.code === 'ER_NO_SUCH_TABLE';
|
||||
return res.status(missingTable ? 503 : 400).json({
|
||||
message: missingTable ? '候选记忆表尚未执行本地迁移' : (err instanceof Error ? err.message : '候选记忆读取失败'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.post('/memory-v2/candidates/:id/accept', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'accepted', {
|
||||
reviewedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.post('/memory-v2/candidates/:id/reject', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'rejected', {
|
||||
reviewedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json(await skillRuntimeConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
const updateSkillRuntimeConfig = async (req, res) => {
|
||||
if (!skillRuntimeConfigService?.updateAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
const result = await skillRuntimeConfigService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
res.json(result);
|
||||
};
|
||||
|
||||
adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
|
||||
adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.listCatalogSummary) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json({ catalog: await skillRuntimeConfigService.listCatalogSummary() });
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
|
||||
});
|
||||
|
||||
adminApi.get('/system-tests/accounts', requireAdmin, async (_req, res) => {
|
||||
if (!systemTestAccountService) {
|
||||
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
||||
|
||||
@@ -29,6 +29,11 @@ export async function bootstrapAdminServices() {
|
||||
updateMindSpaceConfig,
|
||||
} = await importMemind('mindspace-config.mjs');
|
||||
const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-config.mjs');
|
||||
const { createMindSearchConfigService } = await importMemind('mindsearch-config.mjs');
|
||||
const { createPersonalMemoryCandidateStore } = await importMemind('memory-v2-personal-store.mjs');
|
||||
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||
const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs');
|
||||
const { createOpsApi } = await importMemind('admin-routes.mjs');
|
||||
@@ -86,9 +91,18 @@ export async function bootstrapAdminServices() {
|
||||
apiTarget,
|
||||
apiSecret,
|
||||
});
|
||||
await ensureAssetGatewaySchema(pool);
|
||||
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
const mindSearchConfigService = createMindSearchConfigService(pool, { env: process.env });
|
||||
await mindSearchConfigService.ensureSchema();
|
||||
const personalMemoryCandidateStore = createPersonalMemoryCandidateStore(pool);
|
||||
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
h5Root,
|
||||
});
|
||||
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
@@ -133,6 +147,7 @@ export async function bootstrapAdminServices() {
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
@@ -140,6 +155,9 @@ export async function bootstrapAdminServices() {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
@@ -98,12 +98,16 @@ ready
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -120,12 +124,16 @@ ready
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
+10
@@ -8,13 +8,17 @@ import { DashboardPage } from './admin/pages/DashboardPage';
|
||||
import { PoliciesPage } from './admin/pages/PoliciesPage';
|
||||
import { MindSpacePage } from './admin/pages/MindSpacePage';
|
||||
import { MemoryV2Page } from './admin/pages/MemoryV2Page';
|
||||
import { MindSearchPage } from './admin/pages/MindSearchPage';
|
||||
import { ProvidersPage } from './admin/pages/ProvidersPage';
|
||||
import { SkillsPage } from './admin/pages/SkillsPage';
|
||||
import { SystemTestsPage } from './admin/pages/SystemTestsPage';
|
||||
import { UserDetailPage } from './admin/pages/UserDetailPage';
|
||||
import { UsersPage } from './admin/pages/UsersPage';
|
||||
import { WechatPage } from './admin/pages/WechatPage';
|
||||
import { AssetGatewayPage } from './admin/pages/AssetGatewayPage';
|
||||
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
||||
import { AnalyticsConfigPage } from './admin/pages/AnalyticsConfigPage';
|
||||
import { SkillRuntimePage } from './admin/pages/SkillRuntimePage';
|
||||
import { defaultHomePath } from './lib/routes';
|
||||
import { OpsLayout } from './ops/components/OpsLayout';
|
||||
import { AnalyticsPage } from './ops/pages/AnalyticsPage';
|
||||
@@ -119,9 +123,13 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }
|
||||
<Route path="system-tests" element={<SystemTestsPage />} />
|
||||
<Route path="policies" element={<PoliciesPage />} />
|
||||
<Route path="mindspace" element={<MindSpacePage />} />
|
||||
<Route path="analytics" element={<AnalyticsConfigPage />} />
|
||||
<Route path="memory-v2" element={<MemoryV2Page />} />
|
||||
<Route path="mindsearch" element={<MindSearchPage />} />
|
||||
<Route path="skill-runtime" element={<SkillRuntimePage />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
@@ -166,8 +174,10 @@ function loginRedirectPath(pathname: string, role: string | undefined) {
|
||||
|| pathname.startsWith('/policies')
|
||||
|| pathname.startsWith('/mindspace')
|
||||
|| pathname.startsWith('/memory-v2')
|
||||
|| pathname.startsWith('/skill-runtime')
|
||||
|| pathname.startsWith('/providers')
|
||||
|| pathname.startsWith('/wechat')
|
||||
|| pathname.startsWith('/asset-gateway')
|
||||
) {
|
||||
return role === 'admin' || role === undefined ? pathname : '/ops';
|
||||
}
|
||||
|
||||
@@ -28,12 +28,16 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
items: [
|
||||
{ to: '/wechat', label: '服务号' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置' },
|
||||
{ to: '/analytics', label: 'Analytics 配置' },
|
||||
{ to: '/memory-v2', label: 'Memory V2' },
|
||||
{ to: '/mindsearch', label: 'MindSearch' },
|
||||
{ to: '/skill-runtime', label: 'Skill Runtime' },
|
||||
{ to: '/system-tests', label: '系统测试验证' },
|
||||
{ to: '/capabilities', label: '能力' },
|
||||
{ to: '/skills', label: '技能' },
|
||||
{ to: '/policies', label: '策略' },
|
||||
{ to: '/providers', label: '统一模型中心' },
|
||||
{ to: '/asset-gateway', label: '资产能力' },
|
||||
{ to: '/blocked-words', label: '违禁词管理' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
||||
import { getMindSpaceAdminConfig, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client';
|
||||
|
||||
const initial = {
|
||||
enabled: false,
|
||||
websiteId: '',
|
||||
analyticsUrl: 'http://127.0.0.1:3100',
|
||||
domains: '127.0.0.1,localhost',
|
||||
idSecretConfigured: false,
|
||||
};
|
||||
|
||||
export function AnalyticsConfigPage() {
|
||||
const [config, setConfig] = useState(initial);
|
||||
const [secret, setSecret] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
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 load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getMindSpaceAdminConfig();
|
||||
setConfig({ ...initial, ...result.analytics });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载 Analytics 配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
const save = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
try {
|
||||
const result = await updateMindSpaceAdminConfig({
|
||||
analytics: { enabled: config.enabled, websiteId: config.websiteId, analyticsUrl: config.analyticsUrl, domains: config.domains, ...(secret.trim() ? { idSecret: secret.trim() } : {}) },
|
||||
});
|
||||
setConfig({ ...initial, ...result.analytics });
|
||||
setSecret('');
|
||||
setMessage('Analytics 配置已保存;密钥不会在后台回显。');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存 Analytics 配置失败');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openUmami = async () => {
|
||||
setOpening(true);
|
||||
setError(null);
|
||||
try {
|
||||
window.location.assign(await getUmamiSsoUrl());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '无法打开 Umami');
|
||||
setOpening(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="admin-page">
|
||||
<div className="admin-page-head"><h2>Analytics 配置</h2><p className="muted">配置本地 Memind 生成页面的 Umami 统计。默认只连接本机 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>
|
||||
<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>
|
||||
<label className="admin-form-row"><span>Umami 地址</span><input value={config.analyticsUrl} disabled={loading || busy} onChange={(e) => setConfig({ ...config, analyticsUrl: e.target.value })} placeholder="http://127.0.0.1:3100" /></label>
|
||||
<label className="admin-form-row"><span>统计域名</span><input value={config.domains} disabled={loading || busy} onChange={(e) => setConfig({ ...config, domains: e.target.value })} placeholder="127.0.0.1,localhost" /></label>
|
||||
<label className="admin-form-row"><span>匿名化密钥 {config.idSecretConfigured ? '(已配置,留空保持不变)' : ''}</span><input type="password" value={secret} disabled={loading || busy} onChange={(e) => setSecret(e.target.value)} placeholder={config.idSecretConfigured ? '留空保持当前密钥' : '输入本地随机密钥'} autoComplete="new-password" /></label>
|
||||
<div className="admin-actions"><button type="submit" className="send-btn" disabled={busy || loading}>{busy ? '保存中...' : '保存配置'}</button><button type="button" className="ghost-btn" onClick={() => void load()} disabled={busy}>重新加载</button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
getAssetGatewayConfig,
|
||||
listLlmProviderKeys,
|
||||
updateAssetGatewayConfig,
|
||||
updateAssetPluginConfig,
|
||||
} from '../../api/client';
|
||||
import type { AssetGatewayConfig, AssetPluginConfig, LlmProviderKeyRow } from '../../types';
|
||||
|
||||
type PluginDraft = {
|
||||
enabled: boolean;
|
||||
provider: string;
|
||||
llmProviderKeyId: string;
|
||||
llmModel: string;
|
||||
};
|
||||
|
||||
function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||||
return {
|
||||
enabled: plugin.enabled,
|
||||
provider: plugin.provider ?? '',
|
||||
llmProviderKeyId: plugin.llmProviderKeyId ?? '',
|
||||
llmModel: plugin.llmModel ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const pluginVisuals: Record<string, { icon: string; accent: string }> = {
|
||||
'asset-search': { icon: '⌕', accent: 'blue' },
|
||||
'asset-generate': { icon: '✦', accent: 'violet' },
|
||||
'asset-transform': { icon: '✧', accent: 'amber' },
|
||||
'asset-analyze': { icon: '◉', accent: 'green' },
|
||||
};
|
||||
|
||||
export function AssetGatewayPage() {
|
||||
const [config, setConfig] = useState<AssetGatewayConfig | null>(null);
|
||||
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||
const [drafts, setDrafts] = useState<Record<string, PluginDraft>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const activeKeys = useMemo(() => keys.filter((key) => key.status === 'active'), [keys]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [nextConfig, nextKeys] = await Promise.all([getAssetGatewayConfig(), listLlmProviderKeys()]);
|
||||
setConfig(nextConfig);
|
||||
setKeys(nextKeys);
|
||||
setDrafts(Object.fromEntries(nextConfig.plugins.map((plugin) => [plugin.pluginId, draftFromPlugin(plugin)])));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载资产能力配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
const updateDraft = (pluginId: string, patch: Partial<PluginDraft>) => {
|
||||
setDrafts((current) => ({ ...current, [pluginId]: { ...current[pluginId], ...patch } }));
|
||||
};
|
||||
|
||||
const saveGlobal = async (enabled: boolean) => {
|
||||
setBusy('global'); setMessage(null); setError(null);
|
||||
try {
|
||||
const next = await updateAssetGatewayConfig({ enabled });
|
||||
setConfig(next);
|
||||
setMessage(`资产能力总开关已${enabled ? '开启' : '关闭'}。`);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : '保存总开关失败'); }
|
||||
finally { setBusy(null); }
|
||||
};
|
||||
|
||||
const savePlugin = async (plugin: AssetPluginConfig) => {
|
||||
const draft = drafts[plugin.pluginId];
|
||||
if (!draft) return;
|
||||
setBusy(plugin.pluginId); setMessage(null); setError(null);
|
||||
try {
|
||||
const result = await updateAssetPluginConfig(plugin.pluginId, {
|
||||
enabled: draft.enabled,
|
||||
provider: draft.provider || null,
|
||||
llmProviderKeyId: draft.llmProviderKeyId || null,
|
||||
llmModel: draft.llmModel || null,
|
||||
});
|
||||
setConfig(result.config);
|
||||
setDrafts(Object.fromEntries(result.config.plugins.map((item) => [item.pluginId, draftFromPlugin(item)])));
|
||||
setMessage(`${plugin.label}配置已保存。`);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : '保存插件配置失败'); }
|
||||
finally { setBusy(null); }
|
||||
};
|
||||
|
||||
if (loading) return <div className="admin-page"><p className="muted">加载资产能力配置…</p></div>;
|
||||
if (!config) return <div className="admin-page"><p className="banner banner-error">{error ?? '无法加载配置'}</p></div>;
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>资产能力</h2>
|
||||
<p className="muted">可插拔图片、素材与处理能力。默认关闭,不会影响现有聊天或页面生成流程。</p>
|
||||
</div>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-success">{message}</p>}
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">OPTIONAL CAPABILITY</div>
|
||||
<h3>资产能力 Gateway</h3>
|
||||
<p>关闭时所有调用自动降级为现有流程;配置保留,现有聊天和页面生成不受影响。</p>
|
||||
</div>
|
||||
<label className="asset-toggle">
|
||||
<input type="checkbox" checked={config.enabled} disabled={busy === 'global'} onChange={(event) => void saveGlobal(event.target.checked)} />
|
||||
<span>{config.enabled ? '已启用' : '已关闭'}</span>
|
||||
</label>
|
||||
</section>
|
||||
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
||||
{config.plugins.map((plugin) => {
|
||||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
||||
const visual = pluginVisuals[plugin.pluginId] ?? { icon: '✦', accent: 'blue' };
|
||||
return <section className={`asset-plugin-card asset-plugin-card--${visual.accent}`} key={plugin.pluginId}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||||
<div><h3>{plugin.label}</h3><p>{plugin.description}</p></div>
|
||||
<label className="asset-mini-toggle" title={`启用${plugin.label}`}>
|
||||
<input type="checkbox" checked={draft.enabled} onChange={(event) => updateDraft(plugin.pluginId, { enabled: event.target.checked })} />
|
||||
<span>{draft.enabled ? '开启' : '关闭'}</span>
|
||||
</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}>
|
||||
<option value="">选择 Provider</option>
|
||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select></label>
|
||||
{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>)}
|
||||
</select></label>
|
||||
<label>模型<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
||||
<option value="">选择模型</option>
|
||||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||
</select></label>
|
||||
</> : <div className="asset-no-llm">确定性处理 · 不调用 LLM</div>}
|
||||
</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>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,10 @@ const QUICK_LINKS = [
|
||||
{ to: '/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||||
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||||
{ to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' },
|
||||
{ to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
||||
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
||||
{ to: '/skill-runtime', label: 'Skill Runtime', desc: 'H5 manifest 关键词路由开关' },
|
||||
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
||||
{ to: '/capabilities', label: '能力权限', desc: '扩展与工具开关' },
|
||||
] as const;
|
||||
|
||||
+719
-186
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMindSearchConfig, updateMindSearchConfig } from '../../api/client';
|
||||
import type { MindSearchConfig } from '../../types';
|
||||
|
||||
const initial: MindSearchConfig = {
|
||||
enabled: false,
|
||||
mode: 'off',
|
||||
providers: { searxng: false, github: false, reader: false },
|
||||
settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 },
|
||||
};
|
||||
|
||||
export function MindSearchPage() {
|
||||
const [config, setConfig] = useState(initial);
|
||||
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 } });
|
||||
setMeta(result);
|
||||
setMessage('');
|
||||
})
|
||||
.catch((error) => setMessage(error instanceof Error ? error.message : '配置加载失败'));
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
setMessage('保存中…');
|
||||
try {
|
||||
const result = await updateMindSearchConfig(config);
|
||||
setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } });
|
||||
setMeta(result);
|
||||
setMessage('已保存到数据库;新会话生效,旧会话不受影响');
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
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 } }));
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<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>
|
||||
<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>
|
||||
{message && <p role="status">{message}</p>}
|
||||
<p>配置来源:{meta.source === 'admin' ? '数据库' : '环境变量默认值'};最后更新:{meta.updatedAt ? new Date(meta.updatedAt).toLocaleString() : '尚未保存'}{meta.updatedBy ? `;操作人:${meta.updatedBy}` : ''}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { getMindSpaceAdminConfig, updateMindSpaceAdminConfig } from '../../api/c
|
||||
import type { MindSpaceAdminConfig } from '../../types';
|
||||
|
||||
function safeConfig(config: MindSpaceAdminConfig | null): MindSpaceAdminConfig {
|
||||
return config ?? { publicPageLimit: 10 };
|
||||
return config ?? { publicPageLimit: 10, analytics: { enabled: false, websiteId: '', analyticsUrl: 'http://127.0.0.1:3100', domains: '127.0.0.1,localhost', idSecretConfigured: false } };
|
||||
}
|
||||
|
||||
export function MindSpacePage() {
|
||||
|
||||
+215
-160
@@ -35,6 +35,14 @@ import type {
|
||||
|
||||
const CUSTOM_PROVIDER_ID = '__custom__';
|
||||
|
||||
const EXECUTOR_VISUALS: Record<string, { icon: string; accent: string }> = {
|
||||
goose: { icon: 'G', accent: 'blue' },
|
||||
aider: { icon: 'A', accent: 'amber' },
|
||||
openhands: { icon: 'O', accent: 'violet' },
|
||||
};
|
||||
|
||||
const PROVIDER_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
|
||||
|
||||
const defaultProviderForm = {
|
||||
providerId: CUSTOM_PROVIDER_ID,
|
||||
name: '',
|
||||
@@ -270,40 +278,41 @@ function ExecutorCard({
|
||||
}
|
||||
}, [model, selectedKey]);
|
||||
|
||||
const visual = EXECUTOR_VISUALS[binding.executor] ?? { icon: '✦', accent: 'blue' };
|
||||
|
||||
return (
|
||||
<article className="executor-card">
|
||||
<div className="admin-card-head">
|
||||
<section className={`asset-plugin-card asset-plugin-card--${visual.accent} model-center-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||||
<div>
|
||||
<h3>{binding.executorLabel}</h3>
|
||||
<p className="muted">{binding.executorDescription}</p>
|
||||
<p>{binding.executorDescription}</p>
|
||||
</div>
|
||||
<label className="inline-check">
|
||||
<label className="asset-mini-toggle" aria-label={`${binding.executorLabel} 启用状态`}>
|
||||
<input type="checkbox" checked={enabled} onChange={(event) => setEnabled(event.target.checked)} />
|
||||
启用
|
||||
<span>{enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-footer">
|
||||
<div className="model-center-card-body">
|
||||
<div className="executor-meta">
|
||||
<span>{statusText(launchStatus)}</span>
|
||||
{runtime?.ok ? <span>{runtime.providerName} · {runtime.model}</span> : <span>{runtime?.message ?? '运行配置未就绪'}</span>}
|
||||
@@ -348,10 +357,24 @@ function ExecutorCard({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</div>
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</article>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${enabled ? 'is-on' : ''}`}>
|
||||
{enabled ? '绑定启用中' : '绑定已关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -387,45 +410,61 @@ function VisionSection({
|
||||
}, [model, selectedKey]);
|
||||
|
||||
return (
|
||||
<section className="admin-card providers-panel">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p className="muted">发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
<div className="asset-plugin-grid model-center-vision-grid">
|
||||
<section className="asset-plugin-card asset-plugin-card--green model-center-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">◉</div>
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p>发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
</div>
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? `${settings.keyName} · ${settings.visionModel}` : '未配置'}
|
||||
</span>
|
||||
</div>
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
视觉模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!settings?.keyId ? (
|
||||
<p className="model-center-card-note">未配置时,图片消息将由默认模型处理。</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{settings?.keyId ? (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>
|
||||
当前:<strong>{settings.keyName}</strong> · <span className="mono">{settings.visionModel}</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>未配置,图片消息将由默认模型处理</p>
|
||||
)}
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? '已绑定视觉模型' : '等待配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -723,123 +762,139 @@ export function ProvidersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const activeProviderCount = keys.filter((item) => item.status === 'active').length;
|
||||
|
||||
return (
|
||||
<div className="admin-page providers-page">
|
||||
<div className="admin-page-head providers-hero">
|
||||
<div>
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
<div className="providers-hero-meta">
|
||||
<div className="providers-hero-stat">
|
||||
<span className="providers-hero-label">当前默认</span>
|
||||
<strong>{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
</div>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-primary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>执行器绑定</h3>
|
||||
<p className="muted">把 Provider 和模型绑定到 Goose、Aider、OpenHands</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">MODEL CENTER</div>
|
||||
<h3>默认模型与 Provider 资源池</h3>
|
||||
<p>
|
||||
当前默认模型 <strong className="mono">{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
{' · '}
|
||||
Provider 资源池 {activeProviderCount}/{keys.length} 可用
|
||||
</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="executor-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-secondary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>Provider 资源池</h3>
|
||||
<p className="muted">按名称纵向查看,更接近 list</p>
|
||||
</div>
|
||||
<div className="model-center-hero-actions">
|
||||
<button type="button" className="ghost-btn" disabled={busy || loading} onClick={() => void load()}>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="providers-list">
|
||||
{keys.map((row) => (
|
||||
<article key={row.id} className="provider-list-item">
|
||||
<div className="provider-list-main">
|
||||
<div className="provider-list-top">
|
||||
<div className="providers-name">
|
||||
<span>{row.name}</span>
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
</div>
|
||||
<span className={`provider-state ${row.status === 'active' ? 'is-active' : 'is-disabled'}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
<div className="model-center-section-head">
|
||||
<h3 className="model-center-section-title">执行器绑定</h3>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="asset-plugin-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="model-center-section-title">Provider 资源池</h3>
|
||||
<div className="asset-plugin-grid">
|
||||
{keys.map((row, index) => {
|
||||
const accent = PROVIDER_ACCENTS[index % PROVIDER_ACCENTS.length];
|
||||
return (
|
||||
<section key={row.id} className={`asset-plugin-card asset-plugin-card--${accent} model-center-card model-center-provider-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">✦</div>
|
||||
<div>
|
||||
<h3>{row.name}</h3>
|
||||
<p>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</p>
|
||||
</div>
|
||||
<div className="provider-list-meta">
|
||||
<span>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</span>
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="model-center-provider-badges">
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
{row.isVisionSelected ? <span className="risk-pill risk-medium">图片任务</span> : null}
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-fields model-center-provider-meta">
|
||||
<div className="model-center-meta-row">
|
||||
<span>默认模型</span>
|
||||
<span className="mono">{row.defaultModel}</span>
|
||||
</div>
|
||||
<div className="model-center-meta-row">
|
||||
<span>API Key</span>
|
||||
<span className="mono">{row.apiKeyMasked}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-actions providers-actions provider-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.isSelected ? '当前默认 Provider' : '备用配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">图片任务模型</h3>
|
||||
<VisionSection
|
||||
keys={activeKeys}
|
||||
settings={visionSettings}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getSkillRuntimeAdminConfig,
|
||||
listSkillRuntimeCatalog,
|
||||
updateSkillRuntimeAdminConfig,
|
||||
} from '../../api/client';
|
||||
import type { SkillRuntimeAdminConfig, SkillRuntimeCatalogItem } from '../../types';
|
||||
|
||||
export function SkillRuntimePage() {
|
||||
const [config, setConfig] = useState<SkillRuntimeAdminConfig>({
|
||||
router: { v2Enabled: false, manifestRoutingEnabled: false },
|
||||
});
|
||||
const [catalog, setCatalog] = useState<SkillRuntimeCatalogItem[]>([]);
|
||||
const [source, setSource] = useState('default');
|
||||
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [configResult, catalogRows] = await Promise.all([
|
||||
getSkillRuntimeAdminConfig(),
|
||||
listSkillRuntimeCatalog(),
|
||||
]);
|
||||
setConfig(configResult.config);
|
||||
setSource(configResult.source ?? 'default');
|
||||
setUpdatedAt(configResult.updatedAt ?? null);
|
||||
setCatalog(catalogRows);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载 Skill Runtime 配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await updateSkillRuntimeAdminConfig(config);
|
||||
setConfig(result.config);
|
||||
setSource(result.source ?? 'admin-db');
|
||||
setUpdatedAt(result.updatedAt ?? null);
|
||||
setNotice('Skill Runtime 配置已保存。H5 用户刷新或重新登录后会读取新开关。');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const manifestSkillCount = catalog.filter((item) => item.hasManifest && item.triggerKeywords.length > 0).length;
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>Skill Runtime</h2>
|
||||
<p className="muted">控制 H5 Skill Router v2 与 manifest 关键词路由(默认关闭)</p>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="muted">加载中…</p> : null}
|
||||
{error ? <p className="banner banner-error">{error}</p> : null}
|
||||
{notice ? <p className="banner banner-info">{notice}</p> : null}
|
||||
|
||||
{!loading ? (
|
||||
<>
|
||||
<section className="admin-card">
|
||||
<h3>Router 开关</h3>
|
||||
<p className="muted">
|
||||
当前来源:<strong>{source}</strong>
|
||||
{updatedAt
|
||||
? ` · 更新于 ${new Date(updatedAt).toLocaleString('zh-CN', { hour12: false })}`
|
||||
: ''}
|
||||
</p>
|
||||
<p className="muted">
|
||||
开启后仅对已授予 skill 的用户生效;manifest 匹配失败会回退 legacy 路由。
|
||||
</p>
|
||||
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.router.v2Enabled}
|
||||
onChange={(event) => {
|
||||
const v2Enabled = event.target.checked;
|
||||
setConfig({
|
||||
router: {
|
||||
v2Enabled,
|
||||
manifestRoutingEnabled: v2Enabled ? config.router.manifestRoutingEnabled : false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<strong>启用 Skill Router v2</strong>
|
||||
<span className="muted">总开关。关闭时 H5 走 legacy regex 路由。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.router.manifestRoutingEnabled}
|
||||
disabled={!config.router.v2Enabled}
|
||||
onChange={(event) => {
|
||||
setConfig({
|
||||
router: {
|
||||
...config.router,
|
||||
manifestRoutingEnabled: event.target.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<strong>启用 manifest 关键词路由</strong>
|
||||
<span className="muted">
|
||||
读取 skills/*/skill.yaml 的 trigger.keywords。当前已配置 manifest 的 skill:{manifestSkillCount}{' '}
|
||||
个。
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="send-btn" disabled={saving} onClick={() => void handleSave()}>
|
||||
{saving ? '保存中…' : '保存配置'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn-secondary"
|
||||
disabled={loading || saving}
|
||||
onClick={() => void load()}
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<h3>技能 Catalog 预览</h3>
|
||||
<p className="muted">
|
||||
「Manifest」表示该 skill 目录下是否存在 <code>skill.yaml</code>(Router v2 关键词路由配置)。
|
||||
目前 Phase 1 仅 <strong>product-campaign-page</strong> 做了 manifest 试点;其余 skill 仍只有 legacy{' '}
|
||||
<code>SKILL.md</code>,因此 Manifest 显示「否」是正常现象。
|
||||
</p>
|
||||
<p className="muted">有 skill.yaml 且配置了关键词的条目会在 Router v2 开启后参与匹配。</p>
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Skill</th>
|
||||
<th>Manifest</th>
|
||||
<th>关键词</th>
|
||||
<th>Prompt</th>
|
||||
<th>优先级</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{catalog.map((item) => (
|
||||
<tr key={item.name}>
|
||||
<td>
|
||||
<strong>{item.name}</strong>
|
||||
<div className="muted">{item.description}</div>
|
||||
</td>
|
||||
<td>{item.hasManifest ? '是' : '否'}</td>
|
||||
<td>{item.triggerKeywords.length ? item.triggerKeywords.join('、') : '—'}</td>
|
||||
<td>{item.routerPromptKey ?? '—'}</td>
|
||||
<td>{item.routerPriority || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+87
-1
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AssetGatewayConfig,
|
||||
AdminDashboardSummary,
|
||||
AdminServiceRestartAction,
|
||||
AdminServiceRestartResult,
|
||||
@@ -24,13 +25,19 @@ import type {
|
||||
LlmProviderKeyRow,
|
||||
PlanDefinition,
|
||||
PlanSyncResult,
|
||||
PersonalMemoryCandidateListResponse,
|
||||
PolicyDefinition,
|
||||
PolicyMap,
|
||||
PortalUser,
|
||||
MindSpaceAdminConfig,
|
||||
MindSpaceAnalyticsUpdate,
|
||||
MemoryV2AdminConfig,
|
||||
MemoryV2AdminConfigResponse,
|
||||
MemoryV2ModelApiType,
|
||||
MemoryV2RuntimeStatusResponse,
|
||||
SkillRuntimeAdminConfig,
|
||||
SkillRuntimeAdminConfigResponse,
|
||||
SkillRuntimeCatalogItem,
|
||||
SkillDefinition,
|
||||
SkillMap,
|
||||
UsageRecord,
|
||||
@@ -41,6 +48,7 @@ import type {
|
||||
WechatScheduleLlmConfig,
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
MindSearchConfig,
|
||||
} from '../types';
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -279,13 +287,46 @@ export async function updateWechatScheduleLlmConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config');
|
||||
}
|
||||
|
||||
export async function updateAssetGatewayConfig(
|
||||
payload: Pick<AssetGatewayConfig, 'enabled'>,
|
||||
): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAssetPluginConfig(
|
||||
pluginId: string,
|
||||
payload: {
|
||||
enabled: boolean;
|
||||
provider?: string | null;
|
||||
llmProviderKeyId?: string | null;
|
||||
llmModel?: string | null;
|
||||
},
|
||||
): Promise<{ ok: boolean; config: AssetGatewayConfig }> {
|
||||
return portalFetch(`/admin-api/asset-gateway/plugins/${encodeURIComponent(pluginId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMindSpaceAdminConfig(): Promise<MindSpaceAdminConfig> {
|
||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
||||
return result.config;
|
||||
}
|
||||
|
||||
export async function getUmamiSsoUrl(): Promise<string> {
|
||||
const result = await portalFetch<{ url: string }>('/admin-api/analytics/sso');
|
||||
return result.url;
|
||||
}
|
||||
|
||||
export async function updateMindSpaceAdminConfig(
|
||||
payload: Partial<MindSpaceAdminConfig>,
|
||||
payload: Partial<MindSpaceAdminConfig> & { analytics?: MindSpaceAnalyticsUpdate },
|
||||
): Promise<MindSpaceAdminConfig> {
|
||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config', {
|
||||
method: 'PATCH',
|
||||
@@ -298,6 +339,25 @@ export async function getMemoryV2AdminConfig(): Promise<MemoryV2AdminConfigRespo
|
||||
return portalFetch('/admin-api/memory-v2/config');
|
||||
}
|
||||
|
||||
export async function getMemoryV2RuntimeStatus(): Promise<MemoryV2RuntimeStatusResponse> {
|
||||
return portalFetch('/admin-api/memory-v2/status');
|
||||
}
|
||||
|
||||
export async function listPersonalMemoryCandidates(
|
||||
status = 'candidate',
|
||||
): Promise<PersonalMemoryCandidateListResponse> {
|
||||
return portalFetch(`/admin-api/memory-v2/candidates?status=${encodeURIComponent(status)}&limit=50`);
|
||||
}
|
||||
|
||||
export async function reviewPersonalMemoryCandidate(
|
||||
id: string,
|
||||
action: 'accept' | 'reject',
|
||||
): Promise<{ updated: boolean; status: string; reviewedAt: number }> {
|
||||
return portalFetch(`/admin-api/memory-v2/candidates/${encodeURIComponent(id)}/${action}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMemoryV2AdminConfig(
|
||||
payload: Partial<MemoryV2AdminConfig>,
|
||||
): Promise<MemoryV2AdminConfigResponse> {
|
||||
@@ -307,6 +367,24 @@ export async function updateMemoryV2AdminConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSkillRuntimeAdminConfig(): Promise<SkillRuntimeAdminConfigResponse> {
|
||||
return portalFetch('/admin-api/skill-runtime/config');
|
||||
}
|
||||
|
||||
export async function updateSkillRuntimeAdminConfig(
|
||||
config: SkillRuntimeAdminConfig,
|
||||
): Promise<SkillRuntimeAdminConfigResponse> {
|
||||
return portalFetch('/admin-api/skill-runtime/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSkillRuntimeCatalog(): Promise<SkillRuntimeCatalogItem[]> {
|
||||
const result = await portalFetch<{ catalog: SkillRuntimeCatalogItem[] }>('/admin-api/skill-runtime/catalog');
|
||||
return result.catalog ?? [];
|
||||
}
|
||||
|
||||
export async function listMemoryV2ModelOptions(): Promise<{
|
||||
global: LlmGlobalSettings;
|
||||
keys: LlmProviderKeyRow[];
|
||||
@@ -582,6 +660,14 @@ export async function clearUserCapabilityOverrides(userId: string): Promise<void
|
||||
await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function getMindSearchConfig(): Promise<{ config: MindSearchConfig; source: string }> {
|
||||
return portalFetch('/admin-api/mindsearch/config');
|
||||
}
|
||||
|
||||
export async function updateMindSearchConfig(config: Partial<MindSearchConfig>): Promise<{ config: MindSearchConfig; source: string }> {
|
||||
return portalFetch('/admin-api/mindsearch/config', { method: 'PATCH', body: JSON.stringify(config) });
|
||||
}
|
||||
|
||||
// ── Policies ──────────────────────────────────────────
|
||||
|
||||
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
|
||||
|
||||
+460
@@ -950,6 +950,466 @@ body,
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Asset gateway / model center ────────────────────── */
|
||||
|
||||
.asset-gateway-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 72%, #7c5cff);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(120deg, rgba(75, 55, 146, .28), rgba(21, 27, 38, .78));
|
||||
}
|
||||
|
||||
.asset-kicker {
|
||||
color: #aaa1ff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
|
||||
.asset-gateway-hero h3 {
|
||||
margin: 5px 0 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.asset-gateway-hero p {
|
||||
max-width: 620px;
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-toggle,
|
||||
.asset-mini-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-toggle {
|
||||
padding: 9px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .07);
|
||||
}
|
||||
|
||||
.asset-plugin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.asset-plugin-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-bg-surface);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.asset-plugin-grid--balanced .asset-plugin-card {
|
||||
min-height: 326px;
|
||||
}
|
||||
|
||||
.asset-plugin-card--violet { border-top: 3px solid #9374ff; }
|
||||
.asset-plugin-card--blue { border-top: 3px solid #54a8ff; }
|
||||
.asset-plugin-card--amber { border-top: 3px solid #e8af55; }
|
||||
.asset-plugin-card--green { border-top: 3px solid #55c79b; }
|
||||
|
||||
.asset-plugin-card-head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.asset-plugin-icon {
|
||||
display: grid;
|
||||
flex: 0 0 38px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: rgba(132, 110, 255, .17);
|
||||
color: #c4b8ff;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.asset-plugin-card-head h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.asset-plugin-card-head p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.asset-mini-toggle {
|
||||
margin-left: auto;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-status.is-on {
|
||||
color: #68d8a7;
|
||||
}
|
||||
|
||||
.model-center-hero-actions,
|
||||
.model-center-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.model-center-section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.model-center-section-head .model-center-section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-provider-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-card-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-card-note {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-provider-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-provider-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-meta-row .mono {
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.model-center-vision-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.model-center-card .executor-instruction,
|
||||
.model-center-card .executor-meta,
|
||||
.model-center-card .executor-note,
|
||||
.model-center-card .executor-log {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card .executor-actions {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.memory-v2-page {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.memory-v2-page > .asset-gateway-hero,
|
||||
.memory-v2-page > .asset-plugin-grid,
|
||||
.memory-v2-page > .model-center-section-title,
|
||||
.memory-v2-page > .model-center-card-note,
|
||||
.memory-v2-page > .banner {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.memory-v2-page .memory-v2-fields input,
|
||||
.memory-v2-page .memory-v2-fields select,
|
||||
.memory-v2-page .asset-plugin-fields select {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border-input);
|
||||
background: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.memory-v2-top-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-grid {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-card {
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.memory-v2-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-intro {
|
||||
margin: -4px 0 10px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary {
|
||||
padding: 8px 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details .memory-v2-fields {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.memory-v2-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-card .inline-check {
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-runtime-health {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
flex: 1 1 100%;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-runtime-health strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-review {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item small {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.memory-v2-candidate-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-page .model-center-card-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.memory-v2-backend-grid,
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.memory-v2-backend-grid,
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.asset-gateway-hero,
|
||||
.model-center-section-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.asset-plugin-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Capability settings ─────────────────────────────── */
|
||||
|
||||
.capability-intro {
|
||||
|
||||
+125
@@ -195,6 +195,27 @@ export type LlmExecutorLaunchState = {
|
||||
instruction: string | null;
|
||||
};
|
||||
|
||||
export type AssetPluginConfig = {
|
||||
pluginId: string;
|
||||
label: string;
|
||||
description: string;
|
||||
providers: string[];
|
||||
supportsLlm: boolean;
|
||||
enabled: boolean;
|
||||
provider: string | null;
|
||||
llmProviderKeyId: string | null;
|
||||
llmProviderName: string | null;
|
||||
llmProviderId: string | null;
|
||||
llmModel: string | null;
|
||||
updatedAt: number | null;
|
||||
};
|
||||
|
||||
export type AssetGatewayConfig = {
|
||||
enabled: boolean;
|
||||
updatedAt: number | null;
|
||||
plugins: AssetPluginConfig[];
|
||||
};
|
||||
|
||||
export type AdminServiceRestartAction = 'local_restart' | 'pro_restart';
|
||||
|
||||
export type AdminServiceRestartResult = {
|
||||
@@ -260,6 +281,21 @@ export type WechatScheduleLlmConfig = {
|
||||
|
||||
export type MindSpaceAdminConfig = {
|
||||
publicPageLimit: number;
|
||||
analytics: {
|
||||
enabled: boolean;
|
||||
websiteId: string;
|
||||
analyticsUrl: string;
|
||||
domains: string;
|
||||
idSecretConfigured: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type MindSpaceAnalyticsUpdate = {
|
||||
enabled?: boolean;
|
||||
websiteId?: string;
|
||||
analyticsUrl?: string;
|
||||
domains?: string;
|
||||
idSecret?: string;
|
||||
};
|
||||
|
||||
export type MemoryV2AdminSection = Record<string, string | boolean>;
|
||||
@@ -275,6 +311,15 @@ export type MemoryV2AdminConfig = {
|
||||
failOpen?: boolean;
|
||||
};
|
||||
chatIntentRouter: MemoryV2AdminSection;
|
||||
candidateMemory: MemoryV2AdminSection;
|
||||
runtimeControl: MemoryV2AdminSection;
|
||||
policy: MemoryV2AdminSection;
|
||||
retriever: MemoryV2AdminSection;
|
||||
lifecycle: MemoryV2AdminSection;
|
||||
persona: MemoryV2AdminSection;
|
||||
graph: MemoryV2AdminSection;
|
||||
userMemory: MemoryV2AdminSection;
|
||||
pluginHealth: MemoryV2AdminSection;
|
||||
pgvector: MemoryV2AdminSection;
|
||||
qdrant: MemoryV2AdminSection;
|
||||
weaviate: MemoryV2AdminSection;
|
||||
@@ -291,6 +336,79 @@ export type MemoryV2AdminConfigResponse = {
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
export type MemoryV2RuntimeStatusResponse = {
|
||||
ok: boolean;
|
||||
checkedAt?: number;
|
||||
message?: string;
|
||||
memory?: {
|
||||
enabled?: boolean;
|
||||
backend?: string;
|
||||
selectedBackend?: string | null;
|
||||
configSource?: string;
|
||||
configUpdatedAt?: number | null;
|
||||
personalMemory?: {
|
||||
enabled?: boolean;
|
||||
requestedMode?: string;
|
||||
effectiveMode?: string;
|
||||
autoReviewEnabled?: boolean;
|
||||
phase?: string;
|
||||
injectionEnabled?: boolean;
|
||||
persistence?: string;
|
||||
pendingCandidates?: number;
|
||||
health?: Record<string, string | number | boolean | null>;
|
||||
policy?: Record<string, string | number | boolean | null>;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type PersonalMemoryCandidate = {
|
||||
id: string;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
memoryType: string;
|
||||
content: string;
|
||||
importance: number;
|
||||
confidence: number;
|
||||
status: string;
|
||||
policyReason: string;
|
||||
evidence: Record<string, unknown>;
|
||||
reviewedBy: string | null;
|
||||
reviewedAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
export type PersonalMemoryCandidateListResponse = {
|
||||
items: PersonalMemoryCandidate[];
|
||||
counts: Record<string, number>;
|
||||
};
|
||||
|
||||
export type SkillRuntimeAdminConfig = {
|
||||
router: {
|
||||
v2Enabled: boolean;
|
||||
manifestRoutingEnabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SkillRuntimeAdminConfigResponse = {
|
||||
config: SkillRuntimeAdminConfig;
|
||||
updatedAt: number | null;
|
||||
updatedBy: string | null;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type SkillRuntimeCatalogItem = {
|
||||
name: string;
|
||||
dirName: string;
|
||||
description: string;
|
||||
version: string | null;
|
||||
executors: string[];
|
||||
hasManifest: boolean;
|
||||
triggerKeywords: string[];
|
||||
routerPromptKey: string | null;
|
||||
routerPriority: number;
|
||||
};
|
||||
|
||||
export type AdminSystemTestStepStatus = 'passed' | 'warning' | 'failed';
|
||||
|
||||
export type AdminSystemTestStep = {
|
||||
@@ -495,3 +613,10 @@ export type AdminSubscription = {
|
||||
note: string | null;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type MindSearchConfig = {
|
||||
enabled: boolean;
|
||||
mode: 'off' | 'shadow' | 'assist';
|
||||
providers: { searxng: boolean; github: boolean; reader: boolean };
|
||||
settings: { searxngEndpoint: string; maxResults: number; timeoutMs: number; readerMaxChars: number };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user