Compare commits

...

33 Commits

Author SHA1 Message Date
john dd3fed5b14 feat: add Rybbit admin SSO entry 2026-07-24 17:12:31 +08:00
tkmind b009f31aae merge: production-ready Deep Search admin
Merge the verified MindSearch admin configuration and hardened shared-source release gates.
2026-07-24 02:18:48 +00:00
john 4d7bcbe948 fix: harden admin production source gates 2026-07-24 09:53:06 +08:00
john 1b5bbdf467 fix: expose new built-in search services 2026-07-23 22:51:44 +08:00
john 6977f9fc24 feat: select deep search llm in admin 2026-07-23 22:46:14 +08:00
john ce8247743e feat: register local deep search service 2026-07-23 22:27:11 +08:00
john 04859defeb feat: manage pluggable search services 2026-07-23 21:38:41 +08:00
john bacc31c83a Merge branch 'release/episodic-memory-admin-20260722-r3' 2026-07-23 18:23:12 +08:00
john 693fc8a712 Merge remote-tracking branch 'origin/main' 2026-07-23 18:23:09 +08:00
john 00a4855715 fix: wire admin recharge WeChat notifications 2026-07-23 18:14:10 +08:00
tkmind 4f708d10bd Merge pull request 'fix: 等待后台服务真正就绪' (#3) from codex/fix-admin-startup-readiness into main 2026-07-22 05:22:37 +00:00
john 65970f383d fix: wait for admin startup readiness 2026-07-22 13:19:29 +08:00
tkmind f6624936d6 Merge pull request 'fix: 补齐后台共享模块图像运行依赖' (#2) from codex/fix-admin-sharp-runtime into main 2026-07-22 05:17:17 +00:00
john c0d74b9486 fix: include sharp in admin runtime 2026-07-22 13:15:50 +08:00
tkmind 75057bd285 Merge pull request 'feat: 增加历史会话召回灰度控制' (#1) from codex/episodic-memory-rollout-admin into main
Admin build and release gate passed.
2026-07-22 04:59:45 +00:00
john 6e874c7a8e feat: add episodic recall rollout controls 2026-07-22 12:36:30 +08:00
john 790c5794e9 feat: add image_make controls to admin 2026-07-19 21:28:00 +08:00
john 75b7c07a4d fix: keep skill sync on Portal workspace root 2026-07-17 13:58:55 +08:00
john d864bcd6fb fix: package Excel skill controls in admin runtime 2026-07-17 13:54:25 +08:00
john 7c2af3306a feat: add Memind analytics admin and Umami SSO 2026-07-16 14:09:12 +08:00
john 23a5f147fd merge: integrate MindSearch admin controls 2026-07-15 13:47:19 +08:00
john 05681d758e feat: add MindSearch admin controls 2026-07-15 13:46:22 +08:00
john 7df3ae35f1 feat(admin): surface Memory V2 candidate auto-review in management UI
Clarify active/canary/shadow modes and show auto-review metrics so operators do not need to manually approve every user memory.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:16:57 +08:00
john 731d93843d feat(admin): add Memory V2 personal controls and Skill Runtime page
Wire admin UI and API routes to restored Memind personal memory and skill
runtime modules, including candidate review and runtime status display.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:00:20 +08:00
john 4cbacb8529 Merge branch 'codex/space-quota-release'
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 12:18:42 +08:00
john a019da24b0 Merge branch 'codex/asset-gateway-controls' 2026-07-12 12:18:01 +08:00
john 6bd0faac40 refactor(admin): unify model center and Memory V2 as asset plugin cards
Align Providers and Memory V2 pages with asset-gateway card layout; fix
Memory V2 root layout and balanced grid on Asset Gateway.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 12:17:47 +08:00
john 0358484942 feat: present asset plugins as control cards 2026-07-12 10:00:30 +08:00
john 90cbcbba7c feat: bridge asset gateway controls to admin api 2026-07-12 09:54:47 +08:00
john a8a130c08f feat: add asset gateway admin controls 2026-07-12 09:51:20 +08:00
john 98013eb548 Merge branch 'codex/reminder-llm-toggle' 2026-07-07 12:10:36 +08:00
john b9cadd91fc Add wechat reminder LLM admin toggle 2026-07-07 12:10:21 +08:00
john 4b4bc898bd Support editing user space quota in admin 2026-06-25 23:48:29 +08:00
25 changed files with 3560 additions and 356 deletions
-1
View File
@@ -1 +0,0 @@
26747
-1
View File
@@ -1 +0,0 @@
25645
+9
View File
@@ -38,5 +38,14 @@ 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
# Rybbit 仅允许从 MemindAdm 单点登录进入(必须与 105 Rybbit 一致)
MEMIND_RYBBIT_SSO_SECRET=replace-with-the-same-random-secret-used-by-rybbit
RYBBIT_SSO_EMAIL=admin@tkmind.cn
RYBBIT_URL=https://rybbit.tkmind.cn
# Plaza 帖子预览链接
# VITE_PLAZA_BASE=https://plaza.tkmind.cn
+1
View File
@@ -8,4 +8,5 @@ dist-ssr/
memind-lib/
.vite/
*.log
*.pid
logs/
+614
View File
@@ -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",
+1
View File
@@ -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": {
+62 -1
View File
@@ -85,14 +85,27 @@ need_cmd scp
need_cmd rsync
need_cmd tar
need_cmd shasum
need_cmd node
if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then
bash "${ROOT}/scripts/check-release-ready.sh"
ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh"
else
echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2
exit 1
fi
ROOT_GIT_BRANCH="$(git -C "${ROOT}" branch --show-current)"
ROOT_GIT_HEAD="$(git -C "${ROOT}" rev-parse HEAD)"
ROOT_ORIGIN_HEAD="$(git -C "${ROOT}" rev-parse origin/main)"
[[ "${ROOT_GIT_BRANCH}" == "main" ]] || {
echo "memindadm 生产发布必须从 main 执行,当前分支: ${ROOT_GIT_BRANCH:-detached}" >&2
exit 1
}
[[ "${ROOT_GIT_HEAD}" == "${ROOT_ORIGIN_HEAD}" ]] || {
echo "memindadm main 必须与 origin/main 完全一致" >&2
exit 1
}
[[ -f "${IGNORE_FILE}" ]] || {
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
exit 1
@@ -110,6 +123,12 @@ if [[ "${SKIP_BUILD}" -ne 1 ]]; then
)
fi
say "验证共享模块图像运行依赖"
(
cd "${ROOT}"
node --input-type=module -e "const { default: sharp } = await import('sharp'); await sharp({ create: { width: 1, height: 1, channels: 4, background: '#000000' } }).png().toBuffer();"
)
[[ -d "${ROOT}/dist" ]] || {
echo "缺少 dist/,请先本地构建或去掉 --skip-build" >&2
exit 1
@@ -120,6 +139,24 @@ fi
exit 1
}
git -C "${MEMIND_SRC}" fetch origin main --quiet
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD)"
MEMIND_ORIGIN_HEAD="$(git -C "${MEMIND_SRC}" rev-parse origin/main)"
MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current)"
[[ "${MEMIND_GIT_BRANCH}" == "main" ]] || {
echo "Memind 共享模块必须从 main 打包,当前分支: ${MEMIND_GIT_BRANCH:-detached}" >&2
exit 1
}
[[ "${MEMIND_GIT_HEAD}" == "${MEMIND_ORIGIN_HEAD}" ]] || {
echo "Memind main 必须与 origin/main 完全一致" >&2
exit 1
}
[[ -z "$(git -C "${MEMIND_SRC}" status --porcelain=v1 --untracked-files=all)" ]] || {
echo "Memind 共享模块工作区必须干净" >&2
git -C "${MEMIND_SRC}" status --short --untracked-files=all >&2
exit 1
}
say "打包 Memind 共享模块到 memind-lib"
rm -rf "${MEMIND_LIB}"
mkdir -p "${MEMIND_LIB}"
@@ -133,10 +170,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 +217,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"
+15 -4
View File
@@ -34,6 +34,19 @@ export ADM_WEB_HOST="${WEB_HOST}"
export ADM_PORT="${PORT}"
export VITE_BASE_PATH="${BASE_PATH%/}"
wait_for_http() {
local url="$1"
local attempts="${2:-30}"
local attempt
for ((attempt = 1; attempt <= attempts; attempt += 1)); do
if curl -sf --max-time 2 "${url}" >/dev/null; then
return 0
fi
sleep 1
done
return 1
}
if [[ ! -d dist ]]; then
echo "${ROOT}/dist 不存在,请先构建或 rsync dist" >&2
exit 1
@@ -61,9 +74,8 @@ sleep 1
echo "==> 启动 Admin API @ ${API_HOST}:${API_PORT}..."
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
echo $! >"${API_PID_FILE}"
sleep 2
if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
if ! wait_for_http "http://127.0.0.1:${API_PORT}/health" 30; then
echo "❌ Admin API 启动失败,最近日志:" >&2
tail -n 40 "${API_LOG}" >&2 || true
exit 1
@@ -72,11 +84,10 @@ fi
echo "==> 启动 vite preview @ ${WEB_HOST}:${PORT} (base=${BASE_PATH})..."
nohup npx vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 2
preview_url="http://127.0.0.1:${PORT}${BASE_PATH}"
login_probe="http://127.0.0.1:${PORT}/auth/status"
if curl -sf "${preview_url}" >/dev/null && curl -sf "${login_probe}" >/dev/null; then
if wait_for_http "${preview_url}" 30 && wait_for_http "${login_probe}" 15; then
echo "✅ memind_adm 已就绪: ${preview_url}"
curl -sf "${preview_url}" >/dev/null && echo " 首页 OK"
echo " /auth 反代 OK"
+215 -1
View File
@@ -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,17 @@ export function createAdminApp(services) {
const {
userAuth,
llmProviderService,
assetGatewayConfigService,
pool,
ready,
wechatAdmin,
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
adminSystemTestService,
systemTestAccountService,
wordFilterService,
@@ -215,6 +221,32 @@ 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('/analytics/rybbit-sso', requireAdmin, async (_req, res) => {
const sharedSecret = process.env.MEMIND_RYBBIT_SSO_SECRET?.trim();
if (!sharedSecret) return res.status(503).json({ message: '未配置 Rybbit 单点登录密钥' });
const email = process.env.RYBBIT_SSO_EMAIL?.trim().toLowerCase();
if (!email) return res.status(503).json({ message: '未配置 Rybbit 单点登录账号' });
const baseUrl = String(process.env.RYBBIT_URL || 'https://rybbit.tkmind.cn').replace(/\/$/, '');
const encoded = Buffer.from(JSON.stringify({
email,
exp: Math.floor(Date.now() / 1000) + 60,
nonce: crypto.randomUUID(),
})).toString('base64url');
const signature = crypto.createHmac('sha256', sharedSecret).update(encoded).digest('base64url');
res.json({ url: `${baseUrl}/api/auth/memind?ticket=${encoded}.${signature}` });
});
adminApi.get('/users', requireAdmin, async (req, res) => {
const result = await userAuth.listUsers({
page: Number(req.query.page) || 1,
@@ -285,7 +317,27 @@ export function createAdminApp(services) {
adminApi.get('/wechat/summary', requireAdmin, async (_req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
res.json(await wechatAdmin.getSummary());
const summary = await wechatAdmin.getSummary();
const scheduleLlmConfig = wechatScheduleLlmConfigService
? await wechatScheduleLlmConfigService.getConfig()
: { scheduleLlmEnabled: false };
res.json({
...summary,
config: {
...summary.config,
scheduleLlmEnabled: scheduleLlmConfig.scheduleLlmEnabled,
},
});
});
adminApi.patch('/wechat/schedule-llm-config', requireAdmin, async (req, res) => {
if (!wechatScheduleLlmConfigService) {
return res.status(503).json({ message: '服务号提醒 LLM 配置未启用' });
}
const result = await wechatScheduleLlmConfigService.updateConfig(req.body ?? {}, {
updatedBy: req.currentUser.id,
});
res.json(result);
});
adminApi.get('/mindspace/config', requireAdmin, async (_req, res) => {
@@ -300,10 +352,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());
@@ -317,6 +397,140 @@ 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.post('/mindsearch/services/:serviceId/test', requireAdmin, async (req, res) => {
if (!mindSearchConfigService?.testService) return res.status(503).json({ message: 'MindSearch 服务测试未启用' });
try {
return res.json(await mindSearchConfigService.testService(req.params.serviceId));
} catch (error) {
if (error?.code === 'SEARCH_SERVICE_NOT_FOUND') return res.status(404).json({ message: error.message });
throw error;
}
});
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
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: '系统测试账号服务未启用' });
+32
View File
@@ -18,6 +18,7 @@ export async function bootstrapAdminServices() {
const createLlmProviderService = await loadCreateLlmProviderService();
const { createWechatAdminService } = await importMemind('wechat-admin.mjs');
const { createWechatMpService, loadWechatMpConfig } = await importMemind('wechat-mp.mjs');
const { createNotificationDispatcher } = await importMemind('notification-dispatcher.mjs');
const { createPlazaPostService, formatPostRow } = await importMemind('plaza-posts.mjs');
const { createPlazaInteractionService } = await importMemind('plaza-interactions.mjs');
const { createPlazaOpsService } = await importMemind('plaza-ops.mjs');
@@ -29,6 +30,12 @@ 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');
const { createWordFilterService, ensureWordFilterSchema } = await importMemind('word-filter.mjs');
@@ -85,9 +92,21 @@ 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,
});
const adminSystemTestService = createAdminSystemTestService({
pool,
userAuth,
@@ -101,6 +120,14 @@ export async function bootstrapAdminServices() {
throw new Error('admin api does not proxy WeChat chat sessions');
},
});
const notificationDispatcher = createNotificationDispatcher({
sendWechatTextToUser: wechatMpService?.enabled
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
: null,
});
userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => {
await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey });
});
const wechatAdmin = createWechatAdminService(pool, {
config: wechatMpConfig,
scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1',
@@ -129,6 +156,7 @@ export async function bootstrapAdminServices() {
pool,
userAuth,
llmProviderService,
assetGatewayConfigService,
plazaOps,
createOpsApi,
wechatAdmin,
@@ -136,6 +164,10 @@ export async function bootstrapAdminServices() {
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
adminSystemTestService,
systemTestAccountService,
wordFilterService,
+10
View File
@@ -98,12 +98,17 @@ ready
pool,
userAuth,
llmProviderService,
assetGatewayConfigService,
plazaOps,
createOpsApi,
wechatAdmin,
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
adminSystemTestService,
systemTestAccountService,
wordFilterService,
@@ -119,12 +124,17 @@ ready
pool,
userAuth,
llmProviderService,
assetGatewayConfigService,
plazaOps,
createOpsApi,
wechatAdmin,
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
adminSystemTestService,
systemTestAccountService,
wordFilterService,
+10
View File
@@ -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';
}
+4
View File
@@ -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: '违禁词管理' },
],
},
+103
View File
@@ -0,0 +1,103 @@
import { useCallback, useEffect, useState, type FormEvent } from 'react';
import { getMindSpaceAdminConfig, getRybbitSsoUrl, 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 [openingUmami, setOpeningUmami] = useState(false);
const [openingRybbit, setOpeningRybbit] = 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 () => {
setOpeningUmami(true);
setError(null);
try {
window.location.assign(await getUmamiSsoUrl());
} catch (err) {
setError(err instanceof Error ? err.message : '无法打开 Umami');
setOpeningUmami(false);
}
};
const openRybbit = async () => {
setOpeningRybbit(true);
setError(null);
try {
window.location.assign(await getRybbitSsoUrl());
} catch (err) {
setError(err instanceof Error ? err.message : '无法打开 Rybbit');
setOpeningRybbit(false);
}
};
return <div className="admin-page">
<div className="admin-page-head"><h2>Analytics </h2><p className="muted"> Memind Umami </p></div>
{error && <p className="banner banner-error">{error}</p>}
{message && <p className="banner banner-info">{message}</p>}
<section className="admin-card">
<h2>Rybbit </h2>
<p className="muted">访</p>
<div className="admin-actions">
<button type="button" className="send-btn" onClick={() => void openRybbit()} disabled={openingRybbit}>
{openingRybbit ? '正在进入 Rybbit...' : '打开 Rybbit 分析后台'}
</button>
</div>
</section>
<section className="admin-card">
<h2>Umami</h2>
<div className="admin-actions" style={{ marginBottom: 16 }}><button type="button" className="send-btn" onClick={() => void openUmami()} disabled={openingUmami || loading}>{openingUmami ? '正在进入 Umami...' : '打开 Umami 分析后台'}</button></div>
<form className="admin-form" onSubmit={save}>
<label className="admin-form-row"><span></span><input type="checkbox" checked={config.enabled} disabled={loading || busy} onChange={(e) => setConfig({ ...config, enabled: e.target.checked })} /></label>
<label className="admin-form-row"><span>Website ID</span><input value={config.websiteId} disabled={loading || busy} onChange={(e) => setConfig({ ...config, websiteId: e.target.value })} placeholder="Umami Website ID" required={config.enabled} /></label>
<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>;
}
+191
View File
@@ -0,0 +1,191 @@
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;
purposes: Record<string, boolean>;
};
function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
return {
enabled: plugin.enabled,
provider: plugin.provider ?? '',
llmProviderKeyId: plugin.llmProviderKeyId ?? '',
llmModel: plugin.llmModel ?? '',
purposes: { ...(plugin.purposes ?? {}) },
};
}
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 updatePurpose = (pluginId: string, purposeId: string, enabled: boolean) => {
setDrafts((current) => ({
...current,
[pluginId]: {
...current[pluginId],
purposes: { ...(current[pluginId]?.purposes ?? {}), [purposeId]: enabled },
},
}));
};
const saveGlobal = async (enabled: boolean) => {
setBusy('global'); setMessage(null); setError(null);
try {
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,
purposes: draft.purposes,
});
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) => {
const provider = event.target.value;
updateDraft(plugin.pluginId, {
provider,
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
});
}} disabled={!draft.enabled}>
<option value=""> Provider</option>
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
</select></label>
{plugin.pluginId === 'asset-generate' && draft.provider === 'image_make' ? (
<div className="asset-image-make-note">
image_make
</div>
) : plugin.supportsLlm ? <>
<label> LLM<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
<option value="">使 LLM</option>
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
</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>}
{plugin.pluginId === 'asset-generate' && plugin.purposeCatalog?.length ? (
<fieldset className="asset-purpose-fieldset" disabled={!draft.enabled || draft.provider !== 'image_make'}>
<legend>image_make </legend>
<div className="asset-purpose-grid">
{plugin.purposeCatalog.map((purpose) => (
<label key={purpose.id}>
<input
type="checkbox"
checked={Boolean(draft.purposes[purpose.id])}
onChange={(event) => updatePurpose(plugin.pluginId, purpose.id, event.target.checked)}
/>
<span>{purpose.label}<small>{purpose.presetId}</small></span>
</label>
))}
</div>
<p> MindSpace </p>
</fieldset>
) : null}
</div>
<div className="asset-plugin-footer"><span className={`asset-status ${draft.enabled ? 'is-on' : ''}`}>{draft.enabled ? '待保存为启用' : '默认安全关闭'}</span><button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存配置'}</button></div>
</section>;
})}
</div>
</div>
);
}
+2
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+297
View File
@@ -0,0 +1,297 @@
import { useEffect, useState } from 'react';
import {
getMindSearchConfig,
listLlmProviderKeys,
testLlmProviderKey,
testMindSearchService,
updateMindSearchConfig,
} from '../../api/client';
import type {
LlmProviderKeyRow,
MindSearchConfig,
MindSearchRoutes,
MindSearchService,
} from '../../types';
const builtInServices: MindSearchService[] = [
{ id: 'searxng', name: 'SearXNG', kind: 'provider', adapter: 'searxng', capabilities: ['search.web', 'search.news'], endpoint: '', healthPath: '/config', enabled: false, timeoutMs: 8000, priority: 100 },
{ id: 'github', name: 'GitHub Code', kind: 'provider', adapter: 'github', capabilities: ['search.code'], endpoint: 'https://api.github.com/search/code', healthPath: '', enabled: false, timeoutMs: 8000, priority: 90 },
{ id: 'reader', name: 'Safe Reader', kind: 'reader', adapter: 'reader', capabilities: ['search.read'], endpoint: '', healthPath: '', enabled: false, timeoutMs: 8000, priority: 80 },
{ id: 'deep-search', name: 'TKMind Deep Search', kind: 'orchestrator', adapter: 'research-http', capabilities: ['search.web', 'research.plan', 'research.execute'], endpoint: 'http://127.0.0.1:20100', healthPath: '/health', enabled: false, timeoutMs: 120000, priority: 100, llmProviderKeyId: '', llmModel: '' },
];
const initial: MindSearchConfig = {
enabled: false,
mode: 'off',
providers: { searxng: false, github: false, reader: false },
settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 },
services: builtInServices,
routes: { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: 'deep-search' },
};
const routeLabels: Record<keyof MindSearchRoutes, string> = {
web: '普通网页搜索',
news: '新闻搜索',
code: '代码搜索',
read: '网页读取',
research: '深度研究',
};
function mergeConfig(config: MindSearchConfig): MindSearchConfig {
const configuredServices = config.services ?? [];
const configuredServiceIds = new Set(configuredServices.map((service) => service.id));
const services = configuredServices.length
? [
...configuredServices,
...builtInServices.filter((service) => !configuredServiceIds.has(service.id)),
]
: builtInServices;
return {
...initial,
...config,
settings: { ...initial.settings, ...config.settings },
services,
routes: { ...initial.routes, ...config.routes },
};
}
export function MindSearchPage() {
const [config, setConfig] = useState(initial);
const [llmKeys, setLlmKeys] = useState<LlmProviderKeyRow[]>([]);
const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
const [message, setMessage] = useState('加载配置中…');
useEffect(() => {
Promise.all([
getMindSearchConfig(),
listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]),
])
.then(([result, keys]) => {
setConfig(mergeConfig(result.config));
setLlmKeys(keys.filter((key) => key.status === 'active'));
setMeta(result);
setMessage('');
})
.catch((error) => setMessage(error instanceof Error ? error.message : '配置加载失败'));
}, []);
const save = async () => {
setMessage('保存中…');
try {
const result = await updateMindSearchConfig(config);
setConfig(mergeConfig(result.config));
setMeta(result);
setMessage('已保存到数据库;新会话生效,旧会话不受影响');
} catch (error) {
setMessage(error instanceof Error ? error.message : '保存失败');
}
};
const updateSetting = (key: keyof MindSearchConfig['settings'], value: string | number) =>
setConfig((current) => ({ ...current, settings: { ...current.settings, [key]: value } }));
const updateService = (id: string, patch: Partial<MindSearchService>) =>
setConfig((current) => ({
...current,
services: current.services.map((service) => service.id === id ? { ...service, ...patch } : service),
}));
const addService = () => {
const existingIds = new Set(config.services.map((service) => service.id));
let suffix = config.services.length + 1;
while (existingIds.has(`research-${suffix}`)) suffix += 1;
const id = `research-${suffix}`;
setConfig((current) => ({
...current,
services: [...current.services, {
id,
name: 'Research Engine',
kind: 'orchestrator',
adapter: 'research-http',
capabilities: ['search.web', 'research.plan', 'research.execute'],
endpoint: 'http://127.0.0.1:20100',
healthPath: '/health',
enabled: false,
timeoutMs: 30000,
priority: 100,
llmProviderKeyId: '',
llmModel: '',
}],
}));
};
const removeService = (id: string) =>
setConfig((current) => ({
...current,
services: current.services.filter((service) => service.id !== id),
routes: Object.fromEntries(
Object.entries(current.routes).map(([key, value]) => [key, value === id ? '' : value]),
) as unknown as MindSearchRoutes,
}));
const runServiceTest = async (id: string) => {
setMessage(`测试 ${id} 中…`);
try {
const result = await testMindSearchService(id);
setMessage(`${result.ok ? '通过' : '失败'}${id}${result.message}${result.latencyMs}ms${result.resultCount == null ? '' : `${result.resultCount} 条结果`}`);
} catch (error) {
setMessage(error instanceof Error ? error.message : '服务测试失败');
}
};
const runLlmTest = async (service: MindSearchService) => {
if (!service.llmProviderKeyId || !service.llmModel) {
setMessage('请先选择 Deep Search 的 LLM 配置和模型');
return;
}
setMessage(`测试 ${service.llmModel} 中…`);
try {
const result = await testLlmProviderKey(service.llmProviderKeyId, service.llmModel);
setMessage(result.ok
? `LLM 通过:${result.model ?? service.llmModel}${result.latencyMs == null ? '' : `${result.latencyMs}ms`}`
: `LLM 失败:${result.message ?? '模型连接失败'}`);
} catch (error) {
setMessage(error instanceof Error ? error.message : 'LLM 测试失败');
}
};
return (
<section className="admin-page">
<div className="admin-page-header">
<div>
<h1>MindSearch</h1>
<p></p>
</div>
</div>
<div className="admin-card">
<h2></h2>
<div className="admin-form">
<label className="search-service-toggle">
<input
type="checkbox"
checked={config.enabled}
onChange={(event) => setConfig({
...config,
enabled: event.target.checked,
mode: event.target.checked ? (config.mode === 'off' ? 'shadow' : config.mode) : 'off',
})}
/>
<span> MindSearch</span>
</label>
<label>
<span></span>
<select value={config.mode} onChange={(event) => setConfig({ ...config, mode: event.target.value as MindSearchConfig['mode'] })}>
<option value="off"></option>
<option value="shadow">Shadow</option>
<option value="assist">Assist</option>
</select>
</label>
</div>
<h2></h2>
<p> Docker 使</p>
<div className="search-service-grid">
{config.services.map((service) => {
const selectedLlmKey = llmKeys.find((key) => key.id === service.llmProviderKeyId);
return (
<fieldset key={service.id} className="search-service-card">
<legend>{service.name}{service.id}</legend>
<label className="search-service-toggle"><input type="checkbox" checked={service.enabled} onChange={(event) => updateService(service.id, { enabled: event.target.checked })} /><span></span></label>
<div className="admin-form">
<label><span></span><input value={service.name} onChange={(event) => updateService(service.id, { name: event.target.value })} /></label>
<label>
<span></span>
<select value={service.kind} onChange={(event) => updateService(service.id, { kind: event.target.value as MindSearchService['kind'] })}>
<option value="provider">Provider</option>
<option value="reader">Reader</option>
<option value="orchestrator">Orchestrator</option>
</select>
</label>
<label>
<span></span>
<select value={service.adapter} onChange={(event) => updateService(service.id, { adapter: event.target.value as MindSearchService['adapter'] })}>
<option value="searxng">SearXNG</option>
<option value="github">GitHub</option>
<option value="reader">Reader</option>
<option value="research-http">Research HTTP</option>
</select>
</label>
<label><span></span><input value={service.healthPath} placeholder="/health" onChange={(event) => updateService(service.id, { healthPath: event.target.value })} /></label>
<label className="form-span-all"><span>Endpoint</span><input value={service.endpoint} placeholder="http://127.0.0.1:20100" onChange={(event) => updateService(service.id, { endpoint: event.target.value })} /></label>
<label className="form-span-all"><span>Capabilities</span><input value={service.capabilities.join(', ')} onChange={(event) => updateService(service.id, { capabilities: event.target.value.split(',').map((item) => item.trim()).filter(Boolean) })} /></label>
<label><span></span><input type="number" min={1000} max={120000} value={service.timeoutMs} onChange={(event) => updateService(service.id, { timeoutMs: Number(event.target.value) })} /></label>
<label><span></span><input type="number" min={1} max={1000} value={service.priority} onChange={(event) => updateService(service.id, { priority: Number(event.target.value) })} /></label>
{service.adapter === 'research-http' && <>
<label className="form-span-all">
<span> LLM</span>
<select
value={service.llmProviderKeyId ?? ''}
onChange={(event) => {
const key = llmKeys.find((item) => item.id === event.target.value);
updateService(service.id, {
llmProviderKeyId: event.target.value,
llmModel: key?.defaultModel ?? '',
});
}}
>
<option value="">使 LLM</option>
{service.llmProviderKeyId && !selectedLlmKey
&& <option value={service.llmProviderKeyId}> LLM </option>}
{llmKeys.map((key) => (
<option key={key.id} value={key.id}>{key.name} · {key.providerLabel}</option>
))}
</select>
</label>
<label className="form-span-all">
<span></span>
<select
value={service.llmModel ?? ''}
disabled={!selectedLlmKey}
onChange={(event) => updateService(service.id, { llmModel: event.target.value })}
>
{!selectedLlmKey && <option value=""> LLM </option>}
{selectedLlmKey && service.llmModel && !selectedLlmKey.models.includes(service.llmModel)
&& <option value={service.llmModel}>{service.llmModel}</option>}
{selectedLlmKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
</select>
</label>
<p className="search-llm-hint form-span-all"> MindSearch Deep Search </p>
</>}
</div>
<div className="search-service-actions">
<button className="send-btn" type="button" onClick={() => runServiceTest(service.id)}></button>
{service.adapter === 'research-http' && service.llmProviderKeyId && service.llmModel
&& <button type="button" onClick={() => runLlmTest(service)}> LLM</button>}
{!['searxng', 'github', 'reader', 'deep-search'].includes(service.id) && <button type="button" onClick={() => removeService(service.id)}></button>}
</div>
</fieldset>
);
})}
</div>
<button className="send-btn" type="button" onClick={addService}> Research Service</button>
<h2></h2>
<div className="admin-form">
{(Object.keys(routeLabels) as Array<keyof MindSearchRoutes>).map((route) => (
<label key={route}>
<span>{routeLabels[route]}</span>
<select
value={config.routes[route]}
onChange={(event) => setConfig((current) => ({ ...current, routes: { ...current.routes, [route]: event.target.value } }))}
>
<option value=""></option>
{config.services.map((service) => <option key={service.id} value={service.id}>{service.name}{service.id}</option>)}
</select>
</label>
))}
</div>
<h2></h2>
<div className="admin-form">
<label><span></span><input type="number" min={1} max={20} value={config.settings.maxResults} onChange={(event) => updateSetting('maxResults', Number(event.target.value))} /></label>
<label><span> Provider </span><input type="number" min={1000} max={30000} value={config.settings.timeoutMs} onChange={(event) => updateSetting('timeoutMs', Number(event.target.value))} /></label>
<label><span>Reader </span><input type="number" min={1000} max={50000} value={config.settings.readerMaxChars} onChange={(event) => updateSetting('readerMaxChars', Number(event.target.value))} /></label>
</div>
<button className="send-btn" type="button" onClick={save}></button>
{message && <p role="status">{message}</p>}
<p>{meta.source === 'admin' ? '数据库' : '环境变量默认值'}{meta.updatedAt ? new Date(meta.updatedAt).toLocaleString() : '尚未保存'}{meta.updatedBy ? `;操作人:${meta.updatedBy}` : ''}</p>
</div>
</section>
);
}
+1 -1
View File
@@ -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
View File
@@ -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 GooseAiderOpenHands</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}
+186
View File
@@ -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>
);
}
+32
View File
@@ -11,6 +11,7 @@ import {
listWechatMessages,
listWechatWebNotifications,
resumeWechatDigest,
updateWechatScheduleLlmConfig,
} from '../../api/client';
import type {
AdminUserRow,
@@ -38,6 +39,7 @@ function safeSummary(summary: WechatAdminSummary | null) {
bindPath: null,
tokenEndpointConfigured: false,
customerServiceEndpointConfigured: false,
scheduleLlmEnabled: false,
},
counts: summary?.counts ?? {
boundUsers: 0,
@@ -88,6 +90,7 @@ export function WechatPage() {
const [notifyBody, setNotifyBody] = useState('');
const [notifyType, setNotifyType] = useState('manual');
const [notifyChannels, setNotifyChannels] = useState<Array<'web' | 'wechat'>>(['web']);
const [scheduleLlmEnabled, setScheduleLlmEnabled] = useState(false);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -117,6 +120,7 @@ export function WechatPage() {
listAdminUsers({ page: 1, pageSize: 200, status: 'active' }),
]);
setSummary(nextSummary);
setScheduleLlmEnabled(nextSummary.config.scheduleLlmEnabled ?? false);
setBindings(nextBindings);
setMessages(nextMessages);
setDigests(nextDigests);
@@ -224,6 +228,13 @@ export function WechatPage() {
}
};
const handleSaveScheduleLlm = async () => {
await runAction(async () => {
await updateWechatScheduleLlmConfig({ scheduleLlmEnabled });
setNotice(`提醒待办 LLM 已${scheduleLlmEnabled ? '开启' : '关闭'}`);
});
};
return (
<div className="admin-page">
<div className="admin-page-head">
@@ -280,7 +291,28 @@ export function WechatPage() {
<dt> worker</dt>
<dd>{summary.config.reminderWorkerEnabled ? '已启用' : '未启用'}</dd>
</div>
<div>
<dt> LLM</dt>
<dd>{summary.config.scheduleLlmEnabled ? '已开启' : '已关闭'}</dd>
</div>
</dl>
<div className="wechat-toolbar" style={{ marginTop: 16, justifyContent: 'space-between', alignItems: 'center' }}>
<label>
<input
type="checkbox"
checked={scheduleLlmEnabled}
onChange={(event) => setScheduleLlmEnabled(event.target.checked)}
disabled={busy}
/>{' '}
LLM
</label>
<button type="button" className="ghost-btn" onClick={() => void handleSaveScheduleLlm()} disabled={busy}>
{busy ? '保存中…' : '保存开关'}
</button>
</div>
<p className="muted" style={{ marginTop: 8 }}>
LLM
</p>
</section>
)}
+109 -1
View File
@@ -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,
@@ -38,8 +45,11 @@ import type {
WechatBinding,
WechatDeliveryLog,
WechatDigestSubscription,
WechatScheduleLlmConfig,
WechatMessage,
WechatWebNotification,
MindSearchConfig,
MindSearchServiceTestResult,
} from '../types';
export class ApiError extends Error {
@@ -254,6 +264,7 @@ export async function getWechatAdminSummary(): Promise<WechatAdminSummary> {
bindPath: summary.config?.bindPath ?? null,
tokenEndpointConfigured: summary.config?.tokenEndpointConfigured ?? false,
customerServiceEndpointConfigured: summary.config?.customerServiceEndpointConfigured ?? false,
scheduleLlmEnabled: summary.config?.scheduleLlmEnabled ?? false,
},
counts: {
boundUsers: summary.counts?.boundUsers ?? 0,
@@ -268,13 +279,61 @@ export async function getWechatAdminSummary(): Promise<WechatAdminSummary> {
};
}
export async function updateWechatScheduleLlmConfig(
payload: Pick<WechatScheduleLlmConfig, 'scheduleLlmEnabled'>,
): Promise<WechatScheduleLlmConfig> {
return portalFetch<WechatScheduleLlmConfig>('/admin-api/wechat/schedule-llm-config', {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
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;
purposes?: Record<string, boolean>;
},
): 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 getRybbitSsoUrl(): Promise<string> {
const result = await portalFetch<{ url: string }>('/admin-api/analytics/rybbit-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',
@@ -287,6 +346,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> {
@@ -296,6 +374,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[];
@@ -571,6 +667,18 @@ 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) });
}
export async function testMindSearchService(serviceId: string): Promise<MindSearchServiceTestResult> {
return portalFetch(`/admin-api/mindsearch/services/${encodeURIComponent(serviceId)}/test`, { method: 'POST' });
}
// ── Policies ──────────────────────────────────────────
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
+559
View File
@@ -577,6 +577,51 @@ body,
justify-self: stretch;
}
.search-service-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
gap: 14px;
margin: 14px 0;
}
.search-service-card {
min-width: 0;
padding: 14px;
border: 1px solid var(--color-border-input);
border-radius: var(--radius-md);
}
.search-service-card legend {
padding: 0 6px;
font-weight: 600;
}
.search-service-toggle {
display: flex !important;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: var(--color-text-primary) !important;
}
.search-service-toggle input {
width: auto;
margin: 0;
}
.search-service-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.search-llm-hint {
margin: -2px 0 0;
color: var(--color-text-muted);
font-size: 12px;
line-height: 1.55;
}
.form-span-all {
grid-column: 1 / -1;
}
@@ -950,6 +995,520 @@ 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-image-make-note,
.asset-no-llm {
padding: 9px 10px;
border-radius: var(--radius-sm);
background: rgba(132, 110, 255, .1);
color: var(--color-text-muted);
font-size: 11px;
line-height: 1.5;
}
.asset-purpose-fieldset {
min-width: 0;
margin: 2px 0 0;
padding: 10px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
}
.asset-purpose-fieldset legend {
padding: 0 5px;
color: var(--color-text-muted);
font-size: 11px;
}
.asset-purpose-fieldset > p {
margin: 8px 0 0;
color: var(--color-text-muted);
font-size: 10px;
line-height: 1.45;
}
.asset-purpose-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
.asset-purpose-grid label {
display: flex;
align-items: flex-start;
gap: 7px;
padding: 7px;
border-radius: var(--radius-xs);
background: rgba(255, 255, 255, .035);
}
.asset-purpose-grid small {
display: block;
margin-top: 2px;
color: var(--color-text-muted);
font-size: 9px;
word-break: break-all;
}
.asset-plugin-footer {
display: flex;
align-items: center;
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 {
+173
View File
@@ -195,6 +195,33 @@ 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;
purposes?: Record<string, boolean>;
purposeCatalog?: Array<{
id: string;
label: string;
presetId: string;
}>;
};
export type AssetGatewayConfig = {
enabled: boolean;
updatedAt: number | null;
plugins: AssetPluginConfig[];
};
export type AdminServiceRestartAction = 'local_restart' | 'pro_restart';
export type AdminServiceRestartResult = {
@@ -241,6 +268,7 @@ export type WechatAdminSummary = {
bindPath: string | null;
tokenEndpointConfigured: boolean;
customerServiceEndpointConfigured: boolean;
scheduleLlmEnabled: boolean;
};
counts: {
boundUsers: number;
@@ -251,8 +279,29 @@ export type WechatAdminSummary = {
};
};
export type WechatScheduleLlmConfig = {
scheduleLlmEnabled: boolean;
updatedAt: number | null;
updatedBy: string | null;
};
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>;
@@ -268,6 +317,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;
@@ -284,6 +342,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 = {
@@ -488,3 +619,45 @@ 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 };
services: MindSearchService[];
routes: MindSearchRoutes;
};
export type MindSearchService = {
id: string;
name: string;
kind: 'provider' | 'reader' | 'orchestrator';
adapter: 'searxng' | 'github' | 'reader' | 'research-http';
capabilities: string[];
endpoint: string;
healthPath: string;
enabled: boolean;
timeoutMs: number;
priority: number;
llmProviderKeyId?: string;
llmModel?: string;
};
export type MindSearchRoutes = {
web: string;
news: string;
code: string;
read: string;
research: string;
};
export type MindSearchServiceTestResult = {
ok: boolean;
serviceId: string;
adapter: MindSearchService['adapter'];
status: number | null;
latencyMs: number;
resultCount?: number | null;
message: string;
};