chore: 清理临时管理目录并更新文档索引
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
删除 admin-temp-dir 目录及其内容 更新文档索引以包含 dashmap 相关文件
This commit is contained in:
Submodule .claude/worktrees/saas-backend updated: 4d8d560d1f...44256a511c
@@ -1 +0,0 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080/api/v1
|
||||
@@ -1 +0,0 @@
|
||||
VITE_API_BASE_URL=/api/v1
|
||||
24
admin-temp-dir/.gitignore
vendored
24
admin-temp-dir/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,73 +0,0 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>admin-v2</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "admin-v2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.7",
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/pro-components": "^2.8.10",
|
||||
"@ant-design/pro-layout": "^7.22.7",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"antd": "^6.3.4",
|
||||
"axios": "^1.14.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
5008
admin-temp-dir/pnpm-lock.yaml
generated
5008
admin-temp-dir/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -1,53 +0,0 @@
|
||||
import { Component, type ReactNode } from 'react'
|
||||
import { Result, Button } from 'antd'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error('[ErrorBoundary] Unhandled error:', error, info.componentStack)
|
||||
}
|
||||
|
||||
private handleReload = () => {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
private handleReset = () => {
|
||||
this.setState({ hasError: false, error: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<Result
|
||||
status="error"
|
||||
title="页面出现错误"
|
||||
subTitle={this.state.error?.message ?? '发生了未知错误,请刷新页面重试'}
|
||||
extra={[
|
||||
<Button key="retry" onClick={this.handleReset}>重试</Button>,
|
||||
<Button key="reload" type="primary" onClick={this.handleReload}>刷新页面</Button>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// ============================================================
|
||||
// AdminLayout — ProLayout 管理后台布局
|
||||
// ============================================================
|
||||
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import ProLayout from '@ant-design/pro-layout'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
TeamOutlined,
|
||||
CloudServerOutlined,
|
||||
ApiOutlined,
|
||||
KeyOutlined,
|
||||
BarChartOutlined,
|
||||
SwapOutlined,
|
||||
SettingOutlined,
|
||||
FileTextOutlined,
|
||||
MessageOutlined,
|
||||
RobotOutlined,
|
||||
LogoutOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { Avatar, Dropdown, message } from 'antd'
|
||||
import type { MenuDataItem } from '@ant-design/pro-layout'
|
||||
|
||||
const menuConfig: MenuDataItem[] = [
|
||||
{ path: '/', name: '仪表盘', icon: <DashboardOutlined /> },
|
||||
{ path: '/accounts', name: '账号管理', icon: <TeamOutlined />, permission: 'account:admin' },
|
||||
{ path: '/providers', name: '服务商', icon: <CloudServerOutlined />, permission: 'provider:manage' },
|
||||
{ path: '/models', name: '模型管理', icon: <ApiOutlined />, permission: 'model:read' },
|
||||
{ path: '/agent-templates', name: 'Agent 模板', icon: <RobotOutlined />, permission: 'model:read' },
|
||||
{ path: '/api-keys', name: 'API 密钥', icon: <KeyOutlined />, permission: 'admin:full' },
|
||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full' },
|
||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use' },
|
||||
{ path: '/config', name: '系统配置', icon: <SettingOutlined />, permission: 'config:read' },
|
||||
{ path: '/prompts', name: '提示词管理', icon: <MessageOutlined />, permission: 'prompt:read' },
|
||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full' },
|
||||
]
|
||||
|
||||
function filterMenuByPermission(
|
||||
items: MenuDataItem[],
|
||||
hasPermission: (p: string) => boolean,
|
||||
): MenuDataItem[] {
|
||||
return items
|
||||
.filter((item) => !item.permission || hasPermission(item.permission as string))
|
||||
.map(({ permission, ...rest }) => ({
|
||||
...rest,
|
||||
children: rest.children ? filterMenuByPermission(rest.children, hasPermission) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
export default function AdminLayout() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { account, hasPermission, logout } = useAuthStore()
|
||||
|
||||
const menuData = filterMenuByPermission(menuConfig, hasPermission)
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
message.success('已退出登录')
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<ProLayout
|
||||
title="ZCLAW"
|
||||
logo={<div style={{ width: 28, height: 28, background: '#1677ff', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 14 }}>Z</div>}
|
||||
layout="mix"
|
||||
fixSiderbar
|
||||
fixedHeader
|
||||
location={{ pathname: location.pathname }}
|
||||
menuDataRender={() => menuData}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div onClick={() => item.path && navigate(item.path)}>{dom}</div>
|
||||
)}
|
||||
avatarProps={{
|
||||
src: undefined,
|
||||
title: account?.display_name || account?.username || 'Admin',
|
||||
size: 'small',
|
||||
render: (_, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
suppressSiderWhenMenuEmpty
|
||||
contentStyle={{ padding: 24 }}
|
||||
>
|
||||
<Outlet />
|
||||
</ProLayout>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { ConfigProvider, App as AntApp } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import { router } from './router'
|
||||
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 30_000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<ErrorBoundary>
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<AntApp>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
@@ -1,170 +0,0 @@
|
||||
// ============================================================
|
||||
// 账号管理
|
||||
// ============================================================
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { accountService } from '@/services/accounts'
|
||||
import type { AccountPublic } from '@/types'
|
||||
|
||||
const roleLabels: Record<string, string> = {
|
||||
super_admin: '超级管理员',
|
||||
admin: '管理员',
|
||||
user: '用户',
|
||||
}
|
||||
|
||||
const roleColors: Record<string, string> = {
|
||||
super_admin: 'red',
|
||||
admin: 'blue',
|
||||
user: 'default',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
active: '正常',
|
||||
disabled: '已禁用',
|
||||
suspended: '已封禁',
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'green',
|
||||
disabled: 'default',
|
||||
suspended: 'red',
|
||||
}
|
||||
|
||||
export default function Accounts() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['accounts'],
|
||||
queryFn: ({ signal }) => accountService.list(signal),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<AccountPublic> }) =>
|
||||
accountService.update(id, data),
|
||||
onSuccess: () => {
|
||||
message.success('更新成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
||||
setModalOpen(false)
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '更新失败'),
|
||||
})
|
||||
|
||||
const statusMutation = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: AccountPublic['status'] }) =>
|
||||
accountService.updateStatus(id, { status }),
|
||||
onSuccess: () => {
|
||||
message.success('状态更新成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '状态更新失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<AccountPublic>[] = [
|
||||
{ title: '用户名', dataIndex: 'username', width: 120 },
|
||||
{ title: '显示名', dataIndex: 'display_name', width: 120 },
|
||||
{ title: '邮箱', dataIndex: 'email', width: 180 },
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
width: 120,
|
||||
render: (_, record) => <Tag color={roleColors[record.role]}>{roleLabels[record.role] || record.role}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (_, record) => <Tag color={statusColors[record.status]}>{statusLabels[record.status] || record.status}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '2FA',
|
||||
dataIndex: 'totp_enabled',
|
||||
width: 80,
|
||||
render: (_, record) => record.totp_enabled ? <Tag color="green">已启用</Tag> : <Tag>未启用</Tag>,
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'last_login_at',
|
||||
width: 180,
|
||||
render: (_, record) => record.last_login_at ? new Date(record.last_login_at).toLocaleString('zh-CN') : '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
|
||||
编辑
|
||||
</Button>
|
||||
{record.status === 'active' ? (
|
||||
<Popconfirm title="确定禁用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'disabled' })}>
|
||||
<Button size="small" danger>禁用</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Popconfirm title="确定启用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'active' })}>
|
||||
<Button size="small" type="primary">启用</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields()
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: values })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<AccountPublic>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => []}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="编辑账号"
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
|
||||
confirmLoading={updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="display_name" label="显示名">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="邮箱">
|
||||
<Input type="email" />
|
||||
</Form.Item>
|
||||
<Form.Item name="role" label="角色">
|
||||
<Select options={[
|
||||
{ value: 'super_admin', label: '超级管理员' },
|
||||
{ value: 'admin', label: '管理员' },
|
||||
{ value: 'user', label: '用户' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
import { useState } from 'react'
|
||||
@@ -1,190 +0,0 @@
|
||||
// ============================================================
|
||||
// Agent 模板管理
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, Select, InputNumber, Space, Popconfirm, Descriptions } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { agentTemplateService } from '@/services/agent-templates'
|
||||
import type { AgentTemplate } from '@/types'
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
const sourceLabels: Record<string, string> = { builtin: '内置', custom: '自定义' }
|
||||
const visibilityLabels: Record<string, string> = { public: '公开', team: '团队', private: '私有' }
|
||||
const statusLabels: Record<string, string> = { active: '活跃', archived: '已归档' }
|
||||
const statusColors: Record<string, string> = { active: 'green', archived: 'default' }
|
||||
|
||||
export default function AgentTemplates() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [detailRecord, setDetailRecord] = useState<AgentTemplate | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['agent-templates'],
|
||||
queryFn: ({ signal }) => agentTemplateService.list(signal),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Parameters<typeof agentTemplateService.create>[0]) =>
|
||||
agentTemplateService.create(data),
|
||||
onSuccess: () => {
|
||||
message.success('创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-templates'] })
|
||||
setModalOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const archiveMutation = useMutation({
|
||||
mutationFn: (id: string) => agentTemplateService.archive(id),
|
||||
onSuccess: () => {
|
||||
message.success('已归档')
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-templates'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '归档失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<AgentTemplate>[] = [
|
||||
{ title: '名称', dataIndex: 'name', width: 160 },
|
||||
{ title: '分类', dataIndex: 'category', width: 100 },
|
||||
{ title: '模型', dataIndex: 'model', width: 140, render: (_, r) => r.model || '-' },
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
width: 80,
|
||||
render: (_, r) => <Tag>{sourceLabels[r.source] || r.source}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '可见性',
|
||||
dataIndex: 'visibility',
|
||||
width: 80,
|
||||
render: (_, r) => <Tag color="blue">{visibilityLabels[r.visibility] || r.visibility}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status] || r.status}</Tag>,
|
||||
},
|
||||
{ title: '版本', dataIndex: 'current_version', width: 70 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setDetailRecord(record)}>详情</Button>
|
||||
{record.status === 'active' && (
|
||||
<Popconfirm title="确定归档此模板?" onConfirm={() => archiveMutation.mutate(record.id)}>
|
||||
<Button size="small" danger>归档</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleCreate = async () => {
|
||||
const values = await form.validateFields()
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<AgentTemplate>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true) }}>
|
||||
新建模板
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="新建 Agent 模板"
|
||||
open={modalOpen}
|
||||
onOk={handleCreate}
|
||||
onCancel={() => { setModalOpen(false); form.resetFields() }}
|
||||
confirmLoading={createMutation.isPending}
|
||||
width={640}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item name="category" label="分类">
|
||||
<Input placeholder="如 assistant, tool" />
|
||||
</Form.Item>
|
||||
<Form.Item name="model" label="默认模型">
|
||||
<Input placeholder="如 gpt-4o" />
|
||||
</Form.Item>
|
||||
<Form.Item name="system_prompt" label="系统提示词">
|
||||
<TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name="temperature" label="Temperature">
|
||||
<InputNumber min={0} max={2} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="max_tokens" label="最大 Token">
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="visibility" label="可见性">
|
||||
<Select options={[
|
||||
{ value: 'public', label: '公开' },
|
||||
{ value: 'team', label: '团队' },
|
||||
{ value: 'private', label: '私有' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="模板详情"
|
||||
open={!!detailRecord}
|
||||
onCancel={() => setDetailRecord(null)}
|
||||
footer={null}
|
||||
width={640}
|
||||
>
|
||||
{detailRecord && (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
<Descriptions.Item label="名称">{detailRecord.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">{detailRecord.category}</Descriptions.Item>
|
||||
<Descriptions.Item label="模型">{detailRecord.model || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="来源">{sourceLabels[detailRecord.source]}</Descriptions.Item>
|
||||
<Descriptions.Item label="可见性">{visibilityLabels[detailRecord.visibility]}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{statusLabels[detailRecord.status]}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>{detailRecord.description || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="系统提示词" span={2}>
|
||||
<div style={{ whiteSpace: 'pre-wrap', maxHeight: 200, overflow: 'auto' }}>
|
||||
{detailRecord.system_prompt || '-'}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="工具" span={2}>
|
||||
{detailRecord.tools?.map((t) => <Tag key={t}>{t}</Tag>) || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="能力" span={2}>
|
||||
{detailRecord.capabilities?.map((c) => <Tag key={c} color="blue">{c}</Tag>) || '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// ============================================================
|
||||
// API 密钥管理
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, InputNumber, Select, Popconfirm, Space, Typography } from 'antd'
|
||||
import { PlusOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { apiKeyService } from '@/services/api-keys'
|
||||
import type { TokenInfo } from '@/types'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export default function ApiKeys() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [newToken, setNewToken] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['api-keys'],
|
||||
queryFn: ({ signal }) => apiKeyService.list(signal),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: { name: string; expires_days?: number; permissions: string[] }) =>
|
||||
apiKeyService.create(data),
|
||||
onSuccess: (result: TokenInfo) => {
|
||||
message.success('创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
|
||||
if (result.token) {
|
||||
setNewToken(result.token)
|
||||
}
|
||||
setModalOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const revokeMutation = useMutation({
|
||||
mutationFn: (id: string) => apiKeyService.revoke(id),
|
||||
onSuccess: () => {
|
||||
message.success('已撤销')
|
||||
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '撤销失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<TokenInfo>[] = [
|
||||
{ title: '名称', dataIndex: 'name', width: 160 },
|
||||
{ title: '前缀', dataIndex: 'token_prefix', width: 120, render: (_, r) => <Text code>{r.token_prefix}...</Text> },
|
||||
{
|
||||
title: '权限',
|
||||
dataIndex: 'permissions',
|
||||
width: 200,
|
||||
render: (_, r) => r.permissions?.map((p) => <Tag key={p}>{p}</Tag>),
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
dataIndex: 'expires_at',
|
||||
width: 180,
|
||||
render: (_, r) => r.expires_at ? new Date(r.expires_at).toLocaleString('zh-CN') : '永不过期',
|
||||
},
|
||||
{
|
||||
title: '最后使用',
|
||||
dataIndex: 'last_used_at',
|
||||
width: 180,
|
||||
render: (_, r) => r.last_used_at ? new Date(r.last_used_at).toLocaleString('zh-CN') : '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Popconfirm title="确定撤销此密钥?撤销后无法恢复。" onConfirm={() => revokeMutation.mutate(record.id)}>
|
||||
<Button size="small" danger>撤销</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleCreate = async () => {
|
||||
const values = await form.validateFields()
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
|
||||
const copyToken = () => {
|
||||
if (newToken) {
|
||||
navigator.clipboard.writeText(newToken)
|
||||
message.success('已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<TokenInfo>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true) }}>
|
||||
创建密钥
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="创建 API 密钥"
|
||||
open={modalOpen}
|
||||
onOk={handleCreate}
|
||||
onCancel={() => { setModalOpen(false); form.resetFields() }}
|
||||
confirmLoading={createMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="给密钥起个名字" />
|
||||
</Form.Item>
|
||||
<Form.Item name="expires_days" label="有效期 (天)">
|
||||
<InputNumber min={1} placeholder="留空则永不过期" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="permissions" label="权限" rules={[{ required: true }]}>
|
||||
<Select mode="multiple" placeholder="选择权限" options={[
|
||||
{ value: 'relay:use', label: '中转使用' },
|
||||
{ value: 'model:read', label: '模型读取' },
|
||||
{ value: 'config:read', label: '配置读取' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="密钥创建成功"
|
||||
open={!!newToken}
|
||||
onOk={() => setNewToken(null)}
|
||||
onCancel={() => setNewToken(null)}
|
||||
>
|
||||
<p>请立即保存此密钥,关闭后将无法再次查看:</p>
|
||||
<Input.TextArea
|
||||
value={newToken || ''}
|
||||
rows={3}
|
||||
readOnly
|
||||
addonAfter={<CopyOutlined onClick={copyToken} style={{ cursor: 'pointer' }} />}
|
||||
/>
|
||||
<Button type="primary" icon={<CopyOutlined />} onClick={copyToken} style={{ marginTop: 8 }}>
|
||||
复制密钥
|
||||
</Button>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// ============================================================
|
||||
// 系统配置
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Card, Tabs, message, Tag, Input, Button, Space, Typography } from 'antd'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { configService } from '@/services/config'
|
||||
import type { ConfigItem } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
export default function Config() {
|
||||
const queryClient = useQueryClient()
|
||||
const [category, setCategory] = useState<string>('general')
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['config', category],
|
||||
queryFn: ({ signal }) => configService.list({ category }, signal),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, value }: { id: string; value: string }) =>
|
||||
configService.update(id, { value }),
|
||||
onSuccess: () => {
|
||||
message.success('配置已更新')
|
||||
queryClient.invalidateQueries({ queryKey: ['config', category] })
|
||||
setEditingId(null)
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '更新失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<ConfigItem>[] = [
|
||||
{ title: '配置路径', dataIndex: 'key_path', width: 200, render: (_, r) => <code>{r.key_path}</code> },
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'current_value',
|
||||
width: 250,
|
||||
render: (_, record) => {
|
||||
if (editingId === record.id) {
|
||||
return (
|
||||
<Space>
|
||||
<Input
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
onPressEnter={() => updateMutation.mutate({ id: record.id, value: editValue })}
|
||||
/>
|
||||
<Button size="small" type="primary" onClick={() => updateMutation.mutate({ id: record.id, value: editValue })}>
|
||||
保存
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setEditingId(null)}>取消</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
onClick={() => { setEditingId(record.id); setEditValue(record.current_value || '') }}
|
||||
style={{ cursor: 'pointer', color: '#1677ff' }}
|
||||
>
|
||||
{record.current_value || <Tag>未设置</Tag>}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ title: '默认值', dataIndex: 'default_value', width: 200, render: (_, r) => r.default_value || '-' },
|
||||
{ title: '类型', dataIndex: 'value_type', width: 80, render: (_, r) => <Tag>{r.value_type}</Tag> },
|
||||
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '需要重启',
|
||||
dataIndex: 'requires_restart',
|
||||
width: 90,
|
||||
render: (_, r) => r.requires_restart ? <Tag color="orange">是</Tag> : <Tag>否</Tag>,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 24 }}>系统配置</Title>
|
||||
|
||||
<Tabs
|
||||
activeKey={category}
|
||||
onChange={(key) => { setCategory(key); setEditingId(null) }}
|
||||
items={[
|
||||
{ key: 'general', label: '通用' },
|
||||
{ key: 'auth', label: '认证' },
|
||||
{ key: 'relay', label: '中转' },
|
||||
{ key: 'model', label: '模型' },
|
||||
{ key: 'rate_limit', label: '限流' },
|
||||
{ key: 'log', label: '日志' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProTable<ConfigItem>
|
||||
columns={columns}
|
||||
dataSource={data ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// ============================================================
|
||||
// 仪表盘页面
|
||||
// ============================================================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Card, Col, Row, Statistic, Table, Tag, Typography, Spin, Alert } from 'antd'
|
||||
import {
|
||||
TeamOutlined,
|
||||
CloudServerOutlined,
|
||||
ApiOutlined,
|
||||
ThunderboltOutlined,
|
||||
ColumnWidthOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { statsService } from '@/services/stats'
|
||||
import { logService } from '@/services/logs'
|
||||
import type { OperationLog } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
login: '登录', logout: '登出',
|
||||
create_account: '创建账号', update_account: '更新账号', delete_account: '删除账号',
|
||||
create_provider: '创建服务商', update_provider: '更新服务商', delete_provider: '删除服务商',
|
||||
create_model: '创建模型', update_model: '更新模型', delete_model: '删除模型',
|
||||
create_token: '创建密钥', revoke_token: '撤销密钥',
|
||||
update_config: '更新配置',
|
||||
create_prompt: '创建提示词', update_prompt: '更新提示词', archive_prompt: '归档提示词',
|
||||
desktop_audit: '桌面端审计',
|
||||
}
|
||||
|
||||
const actionColors: Record<string, string> = {
|
||||
login: 'green', logout: 'default',
|
||||
create_account: 'blue', update_account: 'orange', delete_account: 'red',
|
||||
create_provider: 'blue', update_provider: 'orange', delete_provider: 'red',
|
||||
create_model: 'blue', update_model: 'orange', delete_model: 'red',
|
||||
create_token: 'blue', revoke_token: 'red',
|
||||
update_config: 'orange',
|
||||
create_prompt: 'blue', update_prompt: 'orange', archive_prompt: 'red',
|
||||
desktop_audit: 'default',
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data: stats, isLoading: statsLoading, error: statsError } = useQuery({
|
||||
queryKey: ['dashboard-stats'],
|
||||
queryFn: ({ signal }) => statsService.dashboard(signal),
|
||||
})
|
||||
|
||||
const { data: logsData, isLoading: logsLoading } = useQuery({
|
||||
queryKey: ['recent-logs'],
|
||||
queryFn: ({ signal }) => logService.list({ page: 1, page_size: 10 }, signal),
|
||||
})
|
||||
|
||||
if (statsError) {
|
||||
return <Alert type="error" message="加载仪表盘数据失败" description={(statsError as Error).message} showIcon />
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{ title: '总账号', value: stats?.total_accounts ?? 0, icon: <TeamOutlined />, color: '#1677ff' },
|
||||
{ title: '活跃服务商', value: stats?.active_providers ?? 0, icon: <CloudServerOutlined />, color: '#52c41a' },
|
||||
{ title: '活跃模型', value: stats?.active_models ?? 0, icon: <ApiOutlined />, color: '#722ed1' },
|
||||
{ title: '今日请求', value: stats?.tasks_today ?? 0, icon: <ThunderboltOutlined />, color: '#fa8c16' },
|
||||
{ title: '今日 Token', value: ((stats?.tokens_today_input ?? 0) + (stats?.tokens_today_output ?? 0)), icon: <ColumnWidthOutlined />, color: '#eb2f96' },
|
||||
]
|
||||
|
||||
const logColumns = [
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
render: (action: string) => (
|
||||
<Tag color={actionColors[action] || 'default'}>
|
||||
{actionLabels[action] || action}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '目标类型', dataIndex: 'target_type', key: 'target_type', width: 100, render: (v: string | null) => v || '-' },
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 24 }}>仪表盘</Title>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{statsLoading ? (
|
||||
<Col span={24}><Spin /></Col>
|
||||
) : (
|
||||
statCards.map((card) => (
|
||||
<Col xs={24} sm={12} md={8} lg={4} key={card.title}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
prefix={<span style={{ color: card.color }}>{card.icon}</span>}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Card title="最近操作日志" size="small">
|
||||
<Table<OperationLog>
|
||||
columns={logColumns}
|
||||
dataSource={logsData?.items ?? []}
|
||||
loading={logsLoading}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// ============================================================
|
||||
// 登录页面
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { LoginForm, ProFormText } from '@ant-design/pro-components'
|
||||
import { LockOutlined, UserOutlined, SafetyOutlined } from '@ant-design/icons'
|
||||
import { message, Divider, Typography } from 'antd'
|
||||
import { authService } from '@/services/auth'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { LoginRequest } from '@/types'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const loginStore = useAuthStore((s) => s.login)
|
||||
const [needTotp, setNeedTotp] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (values: Record<string, string>) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data: LoginRequest = {
|
||||
username: values.username?.trim() || '',
|
||||
password: values.password || '',
|
||||
totp_code: values.totp_code?.trim() || undefined,
|
||||
}
|
||||
|
||||
const res = await authService.login(data)
|
||||
loginStore(res.token, res.refresh_token, res.account)
|
||||
|
||||
message.success('登录成功')
|
||||
const from = searchParams.get('from') || '/'
|
||||
navigate(from, { replace: true })
|
||||
} catch (err: unknown) {
|
||||
const error = err as { message?: string; status?: number }
|
||||
const msg = error.message || ''
|
||||
if (msg.includes('TOTP') || msg.includes('totp') || msg.includes('2FA') || msg.includes('验证码') || error.status === 403) {
|
||||
setNeedTotp(true)
|
||||
message.warning(msg || '请输入两步验证码')
|
||||
} else {
|
||||
message.error(msg || '登录失败,请检查用户名和密码')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex' }}>
|
||||
{/* 左侧品牌区 */}
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #001529 0%, #003a70 50%, #001529 100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Title level={1} style={{ color: '#fff', marginBottom: 8, letterSpacing: 4 }}>
|
||||
ZCLAW
|
||||
</Title>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.65)', fontSize: 16 }}>AI Agent 管理平台</Text>
|
||||
<Divider style={{ borderColor: 'rgba(22,119,255,0.3)', width: 100, minWidth: 100 }} />
|
||||
<Text style={{ color: 'rgba(255,255,255,0.45)', fontSize: 13, maxWidth: 320, textAlign: 'center' }}>
|
||||
统一管理 AI 服务商、模型配置、API 密钥、用量监控与系统配置
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 右侧登录表单 */}
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 480px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 48,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 360 }}>
|
||||
<Title level={3} style={{ marginBottom: 4 }}>登录</Title>
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 32 }}>
|
||||
输入您的账号信息以继续
|
||||
</Text>
|
||||
|
||||
<LoginForm
|
||||
onFinish={handleSubmit}
|
||||
submitter={{
|
||||
searchConfig: { submitText: '登录' },
|
||||
submitButtonProps: { loading, block: true },
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <UserOutlined />,
|
||||
autoComplete: 'username',
|
||||
}}
|
||||
placeholder="请输入用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <LockOutlined />,
|
||||
autoComplete: 'current-password',
|
||||
}}
|
||||
placeholder="请输入密码"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
/>
|
||||
{needTotp && (
|
||||
<ProFormText
|
||||
name="totp_code"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <SafetyOutlined />,
|
||||
maxLength: 6,
|
||||
autoComplete: 'one-time-code',
|
||||
}}
|
||||
placeholder="请输入 6 位验证码"
|
||||
rules={[{ required: true, message: '请输入验证码' }]}
|
||||
/>
|
||||
)}
|
||||
</LoginForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// ============================================================
|
||||
// 操作日志
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Tag, Select, Typography } from 'antd'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { logService } from '@/services/logs'
|
||||
import type { OperationLog } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
login: '登录', logout: '登出',
|
||||
create_account: '创建账号', update_account: '更新账号', delete_account: '删除账号',
|
||||
create_provider: '创建服务商', update_provider: '更新服务商', delete_provider: '删除服务商',
|
||||
create_model: '创建模型', update_model: '更新模型', delete_model: '删除模型',
|
||||
create_token: '创建密钥', revoke_token: '撤销密钥',
|
||||
update_config: '更新配置',
|
||||
create_prompt: '创建提示词', update_prompt: '更新提示词', archive_prompt: '归档提示词',
|
||||
desktop_audit: '桌面端审计',
|
||||
}
|
||||
|
||||
const actionColors: Record<string, string> = {
|
||||
login: 'green', logout: 'default',
|
||||
create_account: 'blue', update_account: 'orange', delete_account: 'red',
|
||||
create_provider: 'blue', update_provider: 'orange', delete_provider: 'red',
|
||||
create_model: 'blue', update_model: 'orange', delete_model: 'red',
|
||||
create_token: 'blue', revoke_token: 'red',
|
||||
update_config: 'orange',
|
||||
create_prompt: 'blue', update_prompt: 'orange', archive_prompt: 'red',
|
||||
desktop_audit: 'default',
|
||||
}
|
||||
|
||||
const actionOptions = Object.entries(actionLabels).map(([value, label]) => ({ value, label }))
|
||||
|
||||
export default function Logs() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [actionFilter, setActionFilter] = useState<string | undefined>(undefined)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['logs', page, actionFilter],
|
||||
queryFn: ({ signal }) => logService.list({ page, page_size: 20, action: actionFilter }, signal),
|
||||
})
|
||||
|
||||
const columns: ProColumns<OperationLog>[] = [
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'action',
|
||||
width: 140,
|
||||
render: (_, r) => (
|
||||
<Tag color={actionColors[r.action] || 'default'}>
|
||||
{actionLabels[r.action] || r.action}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '目标类型', dataIndex: 'target_type', width: 100, render: (_, r) => r.target_type || '-' },
|
||||
{ title: '目标 ID', dataIndex: 'target_id', width: 120, render: (_, r) => r.target_id ? <code>{r.target_id.substring(0, 8)}...</code> : '-' },
|
||||
{
|
||||
title: '详情',
|
||||
dataIndex: 'details',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
render: (_, r) => {
|
||||
if (!r.details) return '-'
|
||||
if (typeof r.details === 'string') return r.details
|
||||
return JSON.stringify(r.details)
|
||||
},
|
||||
},
|
||||
{ title: 'IP 地址', dataIndex: 'ip_address', width: 130, render: (_, r) => <code>{r.ip_address || '-'}</code> },
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>操作日志</Title>
|
||||
<Select
|
||||
value={actionFilter}
|
||||
onChange={(v) => { setActionFilter(v === 'all' ? undefined : v); setPage(1) }}
|
||||
placeholder="操作类型筛选"
|
||||
style={{ width: 160 }}
|
||||
allowClear
|
||||
options={[{ value: 'all', label: '全部操作' }, ...actionOptions]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProTable<OperationLog>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: 20,
|
||||
current: page,
|
||||
onChange: setPage,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
// ============================================================
|
||||
// 模型管理
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, InputNumber, Switch, Select, Space, Popconfirm } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { modelService } from '@/services/models'
|
||||
import { providerService } from '@/services/providers'
|
||||
import type { Model } from '@/types'
|
||||
|
||||
export default function Models() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['models'],
|
||||
queryFn: ({ signal }) => modelService.list(signal),
|
||||
})
|
||||
|
||||
const { data: providersData } = useQuery({
|
||||
queryKey: ['providers-for-select'],
|
||||
queryFn: ({ signal }) => providerService.list(signal),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Omit<Model, 'id'>>) => modelService.create(data),
|
||||
onSuccess: () => {
|
||||
message.success('创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['models'] })
|
||||
setModalOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<Omit<Model, 'id'>> }) =>
|
||||
modelService.update(id, data),
|
||||
onSuccess: () => {
|
||||
message.success('更新成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['models'] })
|
||||
setModalOpen(false)
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '更新失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => modelService.delete(id),
|
||||
onSuccess: () => {
|
||||
message.success('删除成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['models'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<Model>[] = [
|
||||
{ title: '模型 ID', dataIndex: 'model_id', width: 180, render: (_, r) => <code>{r.model_id}</code> },
|
||||
{ title: '别名', dataIndex: 'alias', width: 140 },
|
||||
{
|
||||
title: '服务商',
|
||||
dataIndex: 'provider_id',
|
||||
width: 140,
|
||||
render: (_, r) => {
|
||||
const provider = providersData?.items?.find((p) => p.id === r.provider_id)
|
||||
return provider?.display_name || r.provider_id.substring(0, 8)
|
||||
},
|
||||
},
|
||||
{ title: '上下文窗口', dataIndex: 'context_window', width: 110, render: (_, r) => r.context_window?.toLocaleString() },
|
||||
{ title: '最大输出', dataIndex: 'max_output_tokens', width: 100, render: (_, r) => r.max_output_tokens?.toLocaleString() },
|
||||
{
|
||||
title: '流式',
|
||||
dataIndex: 'supports_streaming',
|
||||
width: 70,
|
||||
render: (_, r) => r.supports_streaming ? <Tag color="green">是</Tag> : <Tag>否</Tag>,
|
||||
},
|
||||
{
|
||||
title: '视觉',
|
||||
dataIndex: 'supports_vision',
|
||||
width: 70,
|
||||
render: (_, r) => r.supports_vision ? <Tag color="blue">是</Tag> : <Tag>否</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 70,
|
||||
render: (_, r) => r.enabled ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确定删除此模型?" onConfirm={() => deleteMutation.mutate(record.id)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields()
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: values })
|
||||
} else {
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<Model>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { setEditingId(null); form.resetFields(); setModalOpen(true) }}>
|
||||
新建模型
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingId ? '编辑模型' : '新建模型'}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
|
||||
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="provider_id" label="服务商" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={(providersData?.items ?? []).map((p) => ({ value: p.id, label: p.display_name }))}
|
||||
placeholder="选择服务商"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="model_id" label="模型 ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 gpt-4o" />
|
||||
</Form.Item>
|
||||
<Form.Item name="alias" label="别名">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="context_window" label="上下文窗口">
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="max_output_tokens" label="最大输出 Token">
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="supports_streaming" label="支持流式" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="supports_vision" label="支持视觉" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="pricing_input" label="输入价格 (每百万 Token)">
|
||||
<InputNumber min={0} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="pricing_output" label="输出价格 (每百万 Token)">
|
||||
<InputNumber min={0} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
// ============================================================
|
||||
// 提示词管理
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, Select, Space, Popconfirm, Descriptions, Tabs, Typography } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { promptService } from '@/services/prompts'
|
||||
import type { PromptTemplate, PromptVersion } from '@/types'
|
||||
|
||||
const { TextArea } = Input
|
||||
const { Text } = Typography
|
||||
|
||||
const sourceLabels: Record<string, string> = { builtin: '内置', custom: '自定义' }
|
||||
const statusLabels: Record<string, string> = { active: '活跃', deprecated: '已废弃', archived: '已归档' }
|
||||
const statusColors: Record<string, string> = { active: 'green', deprecated: 'orange', archived: 'default' }
|
||||
|
||||
export default function Prompts() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [detailName, setDetailName] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['prompts'],
|
||||
queryFn: ({ signal }) => promptService.list(signal),
|
||||
})
|
||||
|
||||
const { data: detailData } = useQuery({
|
||||
queryKey: ['prompt-detail', detailName],
|
||||
queryFn: ({ signal }) => promptService.get(detailName!, signal),
|
||||
enabled: !!detailName,
|
||||
})
|
||||
|
||||
const { data: versionsData } = useQuery({
|
||||
queryKey: ['prompt-versions', detailName],
|
||||
queryFn: ({ signal }) => promptService.listVersions(detailName!, signal),
|
||||
enabled: !!detailName,
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Parameters<typeof promptService.create>[0]) => promptService.create(data),
|
||||
onSuccess: () => {
|
||||
message.success('创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['prompts'] })
|
||||
setCreateOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const archiveMutation = useMutation({
|
||||
mutationFn: (name: string) => promptService.archive(name),
|
||||
onSuccess: () => {
|
||||
message.success('已归档')
|
||||
queryClient.invalidateQueries({ queryKey: ['prompts'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '归档失败'),
|
||||
})
|
||||
|
||||
const rollbackMutation = useMutation({
|
||||
mutationFn: ({ name, version }: { name: string; version: number }) =>
|
||||
promptService.rollback(name, version),
|
||||
onSuccess: () => {
|
||||
message.success('回滚成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['prompts'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['prompt-detail', detailName] })
|
||||
queryClient.invalidateQueries({ queryKey: ['prompt-versions', detailName] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '回滚失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<PromptTemplate>[] = [
|
||||
{ title: '名称', dataIndex: 'name', width: 200, render: (_, r) => <Text code>{r.name}</Text> },
|
||||
{ title: '分类', dataIndex: 'category', width: 100 },
|
||||
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
width: 80,
|
||||
render: (_, r) => <Tag>{sourceLabels[r.source]}</Tag>,
|
||||
},
|
||||
{ title: '版本', dataIndex: 'current_version', width: 70 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status]}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setDetailName(record.name)}>详情</Button>
|
||||
{record.status === 'active' && (
|
||||
<Popconfirm title="确定归档此提示词?" onConfirm={() => archiveMutation.mutate(record.name)}>
|
||||
<Button size="small" danger>归档</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const handleCreate = async () => {
|
||||
const values = await form.validateFields()
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
|
||||
const versionColumns: ProColumns<PromptVersion>[] = [
|
||||
{ title: '版本', dataIndex: 'version', width: 60 },
|
||||
{ title: '更新说明', dataIndex: 'changelog', width: 200, ellipsis: true },
|
||||
{ title: '最低版本', dataIndex: 'min_app_version', width: 100, render: (_, r) => r.min_app_version || '-' },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Popconfirm
|
||||
title={`确定回滚到版本 ${record.version}?`}
|
||||
onConfirm={() => detailName && rollbackMutation.mutate({ name: detailName, version: record.version })}
|
||||
>
|
||||
<Button size="small">回滚</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<PromptTemplate>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setCreateOpen(true) }}>
|
||||
新建提示词
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="新建提示词"
|
||||
open={createOpen}
|
||||
onOk={handleCreate}
|
||||
onCancel={() => { setCreateOpen(false); form.resetFields() }}
|
||||
confirmLoading={createMutation.isPending}
|
||||
width={640}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="唯一标识" />
|
||||
</Form.Item>
|
||||
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 system, tool" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item name="system_prompt" label="系统提示词" rules={[{ required: true }]}>
|
||||
<TextArea rows={6} />
|
||||
</Form.Item>
|
||||
<Form.Item name="user_prompt_template" label="用户提示词模板">
|
||||
<TextArea rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`提示词详情: ${detailName || ''}`}
|
||||
open={!!detailName}
|
||||
onCancel={() => setDetailName(null)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Tabs items={[
|
||||
{
|
||||
key: 'info',
|
||||
label: '基本信息',
|
||||
children: detailData ? (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
<Descriptions.Item label="名称">{detailData.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">{detailData.category}</Descriptions.Item>
|
||||
<Descriptions.Item label="来源">{sourceLabels[detailData.source]}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{statusLabels[detailData.status]}</Descriptions.Item>
|
||||
<Descriptions.Item label="当前版本">{detailData.current_version}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>{detailData.description || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
key: 'versions',
|
||||
label: '版本历史',
|
||||
children: (
|
||||
<ProTable<PromptVersion>
|
||||
columns={versionColumns}
|
||||
dataSource={versionsData ?? []}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={false}
|
||||
size="small"
|
||||
loading={!versionsData}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
// ============================================================
|
||||
// 服务商管理
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, InputNumber, Switch, Space, Popconfirm, Typography } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { providerService } from '@/services/providers'
|
||||
import type { Provider, ProviderKey } from '@/types'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export default function Providers() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [keyModalProviderId, setKeyModalProviderId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['providers'],
|
||||
queryFn: ({ signal }) => providerService.list(signal),
|
||||
})
|
||||
|
||||
const { data: keysData, isLoading: keysLoading } = useQuery({
|
||||
queryKey: ['provider-keys', keyModalProviderId],
|
||||
queryFn: ({ signal }) => providerService.listKeys(keyModalProviderId!, signal),
|
||||
enabled: !!keyModalProviderId,
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>>) =>
|
||||
providerService.create(data),
|
||||
onSuccess: () => {
|
||||
message.success('创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
||||
setModalOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>> }) =>
|
||||
providerService.update(id, data),
|
||||
onSuccess: () => {
|
||||
message.success('更新成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
||||
setModalOpen(false)
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '更新失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => providerService.delete(id),
|
||||
onSuccess: () => {
|
||||
message.success('删除成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<Provider>[] = [
|
||||
{ title: '名称', dataIndex: 'display_name', width: 140 },
|
||||
{ title: '标识', dataIndex: 'name', width: 120, render: (_, r) => <Text code>{r.name}</Text> },
|
||||
{ title: '协议', dataIndex: 'api_protocol', width: 100 },
|
||||
{ title: 'RPM 限制', dataIndex: 'rate_limit_rpm', width: 100, render: (_, r) => r.rate_limit_rpm ?? '-' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
render: (_, r) => r.enabled ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 260,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setKeyModalProviderId(record.id)}>
|
||||
Key Pool
|
||||
</Button>
|
||||
<Popconfirm title="确定删除此服务商?" onConfirm={() => deleteMutation.mutate(record.id)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const keyColumns: ProColumns<ProviderKey>[] = [
|
||||
{ title: '标签', dataIndex: 'key_label', width: 120 },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 80 },
|
||||
{ title: '请求数', dataIndex: 'total_requests', width: 80 },
|
||||
{ title: 'Token 数', dataIndex: 'total_tokens', width: 100 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_active',
|
||||
width: 80,
|
||||
render: (_, r) => r.is_active ? <Tag color="green">活跃</Tag> : <Tag>冷却</Tag>,
|
||||
},
|
||||
]
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields()
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: values })
|
||||
} else {
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<Provider>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { setEditingId(null); form.resetFields(); setModalOpen(true) }}>
|
||||
新建服务商
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? 20,
|
||||
current: data?.page ?? 1,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingId ? '编辑服务商' : '新建服务商'}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
|
||||
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="标识" rules={[{ required: true }]}>
|
||||
<Input disabled={!!editingId} />
|
||||
</Form.Item>
|
||||
<Form.Item name="display_name" label="显示名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="base_url" label="Base URL" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="api_protocol" label="API 协议">
|
||||
<Input placeholder="openai" />
|
||||
</Form.Item>
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="rate_limit_rpm" label="RPM 限制">
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Key Pool"
|
||||
open={!!keyModalProviderId}
|
||||
onCancel={() => setKeyModalProviderId(null)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<ProTable<ProviderKey>
|
||||
columns={keyColumns}
|
||||
dataSource={keysData ?? []}
|
||||
loading={keysLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// ============================================================
|
||||
// 中转任务
|
||||
// ============================================================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Tag, Select, Typography } from 'antd'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { relayService } from '@/services/relay'
|
||||
import { useState } from 'react'
|
||||
import type { RelayTask } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
queued: '排队中',
|
||||
running: '运行中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
queued: 'default',
|
||||
running: 'processing',
|
||||
completed: 'green',
|
||||
failed: 'red',
|
||||
cancelled: 'default',
|
||||
}
|
||||
|
||||
export default function Relay() {
|
||||
const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['relay-tasks', page, statusFilter],
|
||||
queryFn: ({ signal }) => relayService.list({ page, page_size: 20, status: statusFilter }, signal),
|
||||
})
|
||||
|
||||
const columns: ProColumns<RelayTask>[] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 120, render: (_, r) => <code>{r.id.substring(0, 8)}...</code> },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (_, r) => <Tag color={statusColors[r.status] || 'default'}>{statusLabels[r.status] || r.status}</Tag>,
|
||||
},
|
||||
{ title: '模型', dataIndex: 'model_id', width: 160 },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 70 },
|
||||
{ title: '尝试次数', dataIndex: 'attempt_count', width: 80 },
|
||||
{
|
||||
title: 'Token',
|
||||
width: 140,
|
||||
render: (_, r) => `${r.input_tokens.toLocaleString()} / ${r.output_tokens.toLocaleString()}`,
|
||||
},
|
||||
{ title: '错误信息', dataIndex: 'error_message', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '排队时间',
|
||||
dataIndex: 'queued_at',
|
||||
width: 180,
|
||||
render: (_, r) => new Date(r.queued_at).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'completed_at',
|
||||
width: 180,
|
||||
render: (_, r) => r.completed_at ? new Date(r.completed_at).toLocaleString('zh-CN') : '-',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>中转任务</Title>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={(v) => { setStatusFilter(v === 'all' ? undefined : v); setPage(1) }}
|
||||
placeholder="状态筛选"
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
options={[
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'queued', label: '排队中' },
|
||||
{ value: 'running', label: '运行中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProTable<RelayTask>
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={{
|
||||
total: data?.total ?? 0,
|
||||
pageSize: 20,
|
||||
current: page,
|
||||
onChange: setPage,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// ============================================================
|
||||
// 用量统计
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Card, Row, Col, Select, Spin, Alert, Statistic, Typography } from 'antd'
|
||||
import { ColumnWidthOutlined, ThunderboltOutlined } from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { usageService } from '@/services/usage'
|
||||
import { telemetryService } from '@/services/telemetry'
|
||||
import type { DailyUsageStat, ModelUsageStat } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
export default function Usage() {
|
||||
const [days, setDays] = useState(30)
|
||||
|
||||
const { data: dailyData, isLoading: dailyLoading, error: dailyError } = useQuery({
|
||||
queryKey: ['usage-daily', days],
|
||||
queryFn: ({ signal }) => telemetryService.dailyStats({ days }, signal),
|
||||
})
|
||||
|
||||
const { data: modelData, isLoading: modelLoading } = useQuery({
|
||||
queryKey: ['usage-model', days],
|
||||
queryFn: ({ signal }) => telemetryService.modelStats({}, signal),
|
||||
})
|
||||
|
||||
if (dailyError) {
|
||||
return <Alert type="error" message="加载用量数据失败" description={(dailyError as Error).message} showIcon />
|
||||
}
|
||||
|
||||
const totalRequests = dailyData?.reduce((s, d) => s + d.request_count, 0) ?? 0
|
||||
const totalTokens = dailyData?.reduce((s, d) => s + d.input_tokens + d.output_tokens, 0) ?? 0
|
||||
|
||||
const dailyColumns: ProColumns<DailyUsageStat>[] = [
|
||||
{ title: '日期', dataIndex: 'day', width: 120 },
|
||||
{ title: '请求数', dataIndex: 'request_count', width: 100, render: (_, r) => r.request_count.toLocaleString() },
|
||||
{ title: '输入 Token', dataIndex: 'input_tokens', width: 120, render: (_, r) => r.input_tokens.toLocaleString() },
|
||||
{ title: '输出 Token', dataIndex: 'output_tokens', width: 120, render: (_, r) => r.output_tokens.toLocaleString() },
|
||||
{ title: '设备数', dataIndex: 'unique_devices', width: 80 },
|
||||
]
|
||||
|
||||
const modelColumns: ProColumns<ModelUsageStat>[] = [
|
||||
{ title: '模型', dataIndex: 'model_id', width: 200 },
|
||||
{ title: '请求数', dataIndex: 'request_count', width: 100, render: (_, r) => r.request_count.toLocaleString() },
|
||||
{ title: '输入 Token', dataIndex: 'input_tokens', width: 120, render: (_, r) => r.input_tokens.toLocaleString() },
|
||||
{ title: '输出 Token', dataIndex: 'output_tokens', width: 120, render: (_, r) => r.output_tokens.toLocaleString() },
|
||||
{
|
||||
title: '平均延迟',
|
||||
dataIndex: 'avg_latency_ms',
|
||||
width: 100,
|
||||
render: (_, r) => r.avg_latency_ms ? `${Math.round(r.avg_latency_ms)}ms` : '-',
|
||||
},
|
||||
{
|
||||
title: '成功率',
|
||||
dataIndex: 'success_rate',
|
||||
width: 100,
|
||||
render: (_, r) => `${(r.success_rate * 100).toFixed(1)}%`,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>用量统计</Title>
|
||||
<Select
|
||||
value={days}
|
||||
onChange={setDays}
|
||||
options={[
|
||||
{ value: 7, label: '最近 7 天' },
|
||||
{ value: 30, label: '最近 30 天' },
|
||||
{ value: 90, label: '最近 90 天' },
|
||||
]}
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<Statistic title="总请求数" value={totalRequests} prefix={<ThunderboltOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<Statistic title="总 Token 数" value={totalTokens} prefix={<ColumnWidthOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="每日统计" style={{ marginBottom: 24 }} size="small">
|
||||
<ProTable<DailyUsageStat>
|
||||
columns={dailyColumns}
|
||||
dataSource={dailyData ?? []}
|
||||
loading={dailyLoading}
|
||||
rowKey="day"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="按模型统计" size="small">
|
||||
<ProTable<ModelUsageStat>
|
||||
columns={modelColumns}
|
||||
dataSource={modelData ?? []}
|
||||
loading={modelLoading}
|
||||
rowKey="model_id"
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// ============================================================
|
||||
// 路由守卫 — 未登录重定向到 /login
|
||||
// ============================================================
|
||||
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const token = useAuthStore((s) => s.token)
|
||||
const location = useLocation()
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// ============================================================
|
||||
// 路由定义
|
||||
// ============================================================
|
||||
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import { AuthGuard } from './AuthGuard'
|
||||
import AdminLayout from '@/layouts/AdminLayout'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
lazy: () => import('@/pages/Login').then((m) => ({ Component: m.default })),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<AdminLayout />
|
||||
</AuthGuard>
|
||||
),
|
||||
children: [
|
||||
{ index: true, lazy: () => import('@/pages/Dashboard').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'accounts', lazy: () => import('@/pages/Accounts').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'providers', lazy: () => import('@/pages/Providers').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'models', lazy: () => import('@/pages/Models').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'agent-templates', lazy: () => import('@/pages/AgentTemplates').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'api-keys', lazy: () => import('@/pages/ApiKeys').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'prompts', lazy: () => import('@/pages/Prompts').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'logs', lazy: () => import('@/pages/Logs').then((m) => ({ Component: m.default })) },
|
||||
],
|
||||
},
|
||||
])
|
||||
@@ -1,16 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { AccountPublic, PaginatedResponse } from '@/types'
|
||||
|
||||
export const accountService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<AccountPublic>>('/accounts', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
get: (id: string, signal?: AbortSignal) =>
|
||||
request.get<AccountPublic>(`/accounts/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: Partial<Pick<AccountPublic, 'display_name' | 'email' | 'role'>>, signal?: AbortSignal) =>
|
||||
request.patch<AccountPublic>(`/accounts/${id}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
updateStatus: (id: string, data: { status: AccountPublic['status'] }, signal?: AbortSignal) =>
|
||||
request.patch(`/accounts/${id}/status`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { AgentTemplate, PaginatedResponse } from '@/types'
|
||||
|
||||
export const agentTemplateService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<AgentTemplate>>('/agent-templates', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
get: (id: string, signal?: AbortSignal) =>
|
||||
request.get<AgentTemplate>(`/agent-templates/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
create: (data: {
|
||||
name: string; description?: string; category?: string; source?: string
|
||||
model?: string; system_prompt?: string; tools?: string[]
|
||||
capabilities?: string[]; temperature?: number; max_tokens?: number
|
||||
visibility?: string
|
||||
}, signal?: AbortSignal) =>
|
||||
request.post<AgentTemplate>('/agent-templates', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: {
|
||||
description?: string; model?: string; system_prompt?: string
|
||||
tools?: string[]; capabilities?: string[]; temperature?: number
|
||||
max_tokens?: number; visibility?: string; status?: string
|
||||
}, signal?: AbortSignal) =>
|
||||
request.post<AgentTemplate>(`/agent-templates/${id}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
archive: (id: string, signal?: AbortSignal) =>
|
||||
request.delete<AgentTemplate>(`/agent-templates/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { TokenInfo, CreateTokenRequest, PaginatedResponse } from '@/types'
|
||||
|
||||
export const apiKeyService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<TokenInfo>>('/keys', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
create: (data: CreateTokenRequest, signal?: AbortSignal) =>
|
||||
request.post<TokenInfo>('/keys', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
revoke: (id: string, signal?: AbortSignal) =>
|
||||
request.delete(`/keys/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { AccountPublic, LoginRequest, LoginResponse } from '@/types'
|
||||
|
||||
export const authService = {
|
||||
login: (data: LoginRequest, signal?: AbortSignal) =>
|
||||
request.post<LoginResponse>('/auth/login', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
me: (signal?: AbortSignal) =>
|
||||
request.get<AccountPublic>('/auth/me', withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { ConfigItem, PaginatedResponse } from '@/types'
|
||||
|
||||
export const configService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<ConfigItem>>('/config/items', withSignal({ params }, signal))
|
||||
.then((r) => r.data.items),
|
||||
|
||||
update: (id: string, data: { value: string | number | boolean }, signal?: AbortSignal) =>
|
||||
request.patch<ConfigItem>(`/config/items/${id}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { OperationLog, PaginatedResponse } from '@/types'
|
||||
|
||||
export const logService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<OperationLog>>('/logs/operations', withSignal({ params }, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { Model, PaginatedResponse } from '@/types'
|
||||
|
||||
export const modelService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<Model>>('/models', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
create: (data: Partial<Omit<Model, 'id'>>, signal?: AbortSignal) =>
|
||||
request.post<Model>('/models', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: Partial<Omit<Model, 'id'>>, signal?: AbortSignal) =>
|
||||
request.patch<Model>(`/models/${id}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
delete: (id: string, signal?: AbortSignal) =>
|
||||
request.delete(`/models/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { PromptTemplate, PromptVersion, PaginatedResponse } from '@/types'
|
||||
|
||||
export const promptService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<PromptTemplate>>('/prompts', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
get: (name: string, signal?: AbortSignal) =>
|
||||
request.get<PromptTemplate>(`/prompts/${encodeURIComponent(name)}`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
create: (data: {
|
||||
name: string; category: string; description?: string; source?: string
|
||||
system_prompt: string; user_prompt_template?: string
|
||||
variables?: unknown[]; min_app_version?: string
|
||||
}, signal?: AbortSignal) =>
|
||||
request.post<PromptTemplate>('/prompts', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
update: (name: string, data: { description?: string; status?: string }, signal?: AbortSignal) =>
|
||||
request.put<PromptTemplate>(`/prompts/${encodeURIComponent(name)}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
archive: (name: string, signal?: AbortSignal) =>
|
||||
request.delete<PromptTemplate>(`/prompts/${encodeURIComponent(name)}`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
listVersions: (name: string, signal?: AbortSignal) =>
|
||||
request.get<PromptVersion[]>(`/prompts/${encodeURIComponent(name)}/versions`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
createVersion: (name: string, data: {
|
||||
system_prompt: string; user_prompt_template?: string
|
||||
variables?: unknown[]; changelog?: string; min_app_version?: string
|
||||
}, signal?: AbortSignal) =>
|
||||
request.post<PromptVersion>(`/prompts/${encodeURIComponent(name)}/versions`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
rollback: (name: string, version: number, signal?: AbortSignal) =>
|
||||
request.post<PromptTemplate>(`/prompts/${encodeURIComponent(name)}/rollback/${version}`, undefined, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { Provider, ProviderKey, PaginatedResponse } from '@/types'
|
||||
|
||||
export const providerService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<Provider>>('/providers', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
create: (data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>>, signal?: AbortSignal) =>
|
||||
request.post<Provider>('/providers', data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
update: (id: string, data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>>, signal?: AbortSignal) =>
|
||||
request.patch<Provider>(`/providers/${id}`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
delete: (id: string, signal?: AbortSignal) =>
|
||||
request.delete(`/providers/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
listKeys: (providerId: string, signal?: AbortSignal) =>
|
||||
request.get<ProviderKey[]>(`/providers/${providerId}/keys`, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
addKey: (providerId: string, data: {
|
||||
key_label: string; key_value: string; priority?: number
|
||||
max_rpm?: number; max_tpm?: number; quota_reset_interval?: string
|
||||
}, signal?: AbortSignal) =>
|
||||
request.post<{ ok: boolean; key_id: string }>(`/providers/${providerId}/keys`, data, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
toggleKey: (providerId: string, keyId: string, active: boolean, signal?: AbortSignal) =>
|
||||
request.put<{ ok: boolean }>(`/providers/${providerId}/keys/${keyId}/toggle`, { active }, withSignal({}, signal)).then((r) => r.data),
|
||||
|
||||
deleteKey: (providerId: string, keyId: string, signal?: AbortSignal) =>
|
||||
request.delete<{ ok: boolean }>(`/providers/${providerId}/keys/${keyId}`, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { RelayTask, PaginatedResponse } from '@/types'
|
||||
|
||||
export const relayService = {
|
||||
list: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<PaginatedResponse<RelayTask>>('/relay/tasks', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
get: (id: string, signal?: AbortSignal) =>
|
||||
request.get<RelayTask>(`/relay/tasks/${id}`, withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// ============================================================
|
||||
// ZCLAW Admin V2 — Axios 实例 + JWT 拦截器
|
||||
// ============================================================
|
||||
//
|
||||
// 认证策略: 主路径使用 HttpOnly cookie(浏览器自动附加),
|
||||
// Authorization header 作为 fallback 保留用于 API 客户端。
|
||||
|
||||
import axios from 'axios'
|
||||
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { ApiError } from '@/types'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'
|
||||
const TIMEOUT_MS = 30_000
|
||||
|
||||
/** API 业务错误 */
|
||||
export class ApiRequestError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public body: ApiError,
|
||||
) {
|
||||
super(body.message || `Request failed with status ${status}`)
|
||||
this.name = 'ApiRequestError'
|
||||
}
|
||||
}
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: TIMEOUT_MS,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true, // 发送 HttpOnly cookies
|
||||
})
|
||||
|
||||
// ── 请求拦截器:附加 Authorization header fallback ──────────
|
||||
|
||||
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const token = useAuthStore.getState().token
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// ── 响应拦截器:401 自动刷新 ──────────────────────────────
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
|
||||
function onTokenRefreshed(newToken: string) {
|
||||
pendingRequests.forEach((cb) => cb(newToken))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError<{ error?: string; message?: string }>) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
|
||||
// 401 → 尝试刷新 Token
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
const store = useAuthStore.getState()
|
||||
if (!store.refreshToken) {
|
||||
store.logout()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${BASE_URL}/auth/refresh`, null, {
|
||||
headers: { Authorization: `Bearer ${store.refreshToken}` },
|
||||
withCredentials: true, // 发送 refresh cookie
|
||||
})
|
||||
const newToken = res.data.token as string
|
||||
const newRefreshToken = res.data.refresh_token as string
|
||||
// 更新内存中的 token(实际认证通过 HttpOnly cookie,浏览器已自动更新)
|
||||
store.setToken(newToken)
|
||||
if (newRefreshToken) {
|
||||
store.setRefreshToken(newRefreshToken)
|
||||
}
|
||||
onTokenRefreshed(newToken)
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
store.logout()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
// 构造 ApiRequestError
|
||||
if (error.response) {
|
||||
const body: ApiError = {
|
||||
error: error.response.data?.error || 'unknown',
|
||||
message: error.response.data?.message || `请求失败 (${error.response.status})`,
|
||||
status: error.response.status,
|
||||
}
|
||||
return Promise.reject(new ApiRequestError(error.response.status, body))
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default request
|
||||
|
||||
/** 将 AbortSignal 注入 Axios config,用于 TanStack Query 的请求取消 */
|
||||
export function withSignal(config: AxiosRequestConfig = {}, signal?: AbortSignal): AxiosRequestConfig {
|
||||
if (signal) {
|
||||
return { ...config, signal }
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { DashboardStats } from '@/types'
|
||||
|
||||
export const statsService = {
|
||||
dashboard: (signal?: AbortSignal) =>
|
||||
request.get<DashboardStats>('/stats/dashboard', withSignal({}, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { ModelUsageStat, DailyUsageStat } from '@/types'
|
||||
|
||||
export const telemetryService = {
|
||||
modelStats: (params?: Record<string, unknown>, signal?: AbortSignal) =>
|
||||
request.get<ModelUsageStat[]>('/telemetry/stats', withSignal({ params }, signal)).then((r) => r.data),
|
||||
|
||||
dailyStats: (params?: { days?: number }, signal?: AbortSignal) =>
|
||||
request.get<DailyUsageStat[]>('/telemetry/daily', withSignal({ params }, signal)).then((r) => r.data),
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import request, { withSignal } from './request'
|
||||
import type { UsageRecord, UsageByModel } from '@/types'
|
||||
|
||||
export const usageService = {
|
||||
daily: (params?: { days?: number }, signal?: AbortSignal) =>
|
||||
request.get<{ by_day: UsageRecord[] }>('/usage', withSignal({ params: { ...params, group_by: 'day' } }, signal))
|
||||
.then((r) => r.data.by_day || []),
|
||||
|
||||
byModel: (params?: { days?: number }, signal?: AbortSignal) =>
|
||||
request.get<{ by_model: UsageByModel[] }>('/usage', withSignal({ params: { ...params, group_by: 'model' } }, signal))
|
||||
.then((r) => r.data.by_model || []),
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// ============================================================
|
||||
// ZCLAW Admin V2 — Zustand 认证状态管理
|
||||
// ============================================================
|
||||
//
|
||||
// 安全策略: JWT token 通过 HttpOnly cookie 传递,前端 JS 无法读取。
|
||||
// account 信息(显示名/角色)仍存 localStorage 用于页面刷新后恢复 UI。
|
||||
// 内存中的 token/refreshToken 仅用于 Authorization header fallback(API 客户端兼容)。
|
||||
|
||||
import { create } from 'zustand'
|
||||
import type { AccountPublic } from '@/types'
|
||||
|
||||
/** 权限常量 — 与后端 db.rs SEED_ROLES 保持同步 */
|
||||
const ROLE_PERMISSIONS: Record<string, string[]> = {
|
||||
super_admin: [
|
||||
'admin:full', 'account:admin', 'provider:manage', 'model:manage',
|
||||
'relay:admin', 'config:write', 'prompt:read', 'prompt:write',
|
||||
'prompt:publish', 'prompt:admin',
|
||||
],
|
||||
admin: [
|
||||
'account:read', 'account:admin', 'provider:manage', 'model:read',
|
||||
'model:manage', 'relay:use', 'config:read',
|
||||
'config:write', 'prompt:read', 'prompt:write', 'prompt:publish',
|
||||
],
|
||||
user: ['model:read', 'relay:use', 'config:read', 'prompt:read'],
|
||||
}
|
||||
|
||||
const ACCOUNT_KEY = 'zclaw_admin_account'
|
||||
|
||||
/** 从 localStorage 恢复 account 信息(token 通过 HttpOnly cookie 管理) */
|
||||
function loadFromStorage(): { account: AccountPublic | null } {
|
||||
const raw = localStorage.getItem(ACCOUNT_KEY)
|
||||
let account: AccountPublic | null = null
|
||||
if (raw) {
|
||||
try { account = JSON.parse(raw) } catch { /* ignore */ }
|
||||
}
|
||||
return { account }
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
refreshToken: string | null
|
||||
account: AccountPublic | null
|
||||
permissions: string[]
|
||||
|
||||
setToken: (token: string) => void
|
||||
setRefreshToken: (refreshToken: string) => void
|
||||
login: (token: string, refreshToken: string, account: AccountPublic) => void
|
||||
logout: () => void
|
||||
hasPermission: (permission: string) => boolean
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => {
|
||||
const stored = loadFromStorage()
|
||||
const perms = stored.account?.role
|
||||
? (ROLE_PERMISSIONS[stored.account.role] ?? [])
|
||||
: []
|
||||
|
||||
return {
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
account: stored.account,
|
||||
permissions: perms,
|
||||
|
||||
setToken: (token: string) => {
|
||||
set({ token })
|
||||
},
|
||||
|
||||
setRefreshToken: (refreshToken: string) => {
|
||||
set({ refreshToken })
|
||||
},
|
||||
|
||||
login: (token: string, refreshToken: string, account: AccountPublic) => {
|
||||
// account 保留 localStorage(仅用于 UI 显示,非敏感)
|
||||
localStorage.setItem(ACCOUNT_KEY, JSON.stringify(account))
|
||||
// token 仅存内存(实际认证通过 HttpOnly cookie)
|
||||
set({
|
||||
token,
|
||||
refreshToken,
|
||||
account,
|
||||
permissions: ROLE_PERMISSIONS[account.role] ?? [],
|
||||
})
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem(ACCOUNT_KEY)
|
||||
set({ token: null, refreshToken: null, account: null, permissions: [] })
|
||||
// 调用后端 logout 清除 HttpOnly cookies(fire-and-forget)
|
||||
fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||||
},
|
||||
|
||||
hasPermission: (permission: string) => {
|
||||
const { permissions } = get()
|
||||
return permissions.includes(permission) || permissions.includes('admin:full')
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,267 +0,0 @@
|
||||
// ============================================================
|
||||
// ZCLAW SaaS Admin — 全局类型定义
|
||||
// ============================================================
|
||||
|
||||
/** 公共账号信息 */
|
||||
export interface AccountPublic {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
display_name: string
|
||||
role: 'super_admin' | 'admin' | 'user'
|
||||
status: 'active' | 'disabled' | 'suspended'
|
||||
totp_enabled: boolean
|
||||
last_login_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 登录请求 */
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
totp_code?: string
|
||||
}
|
||||
|
||||
/** 登录响应 */
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
refresh_token: string
|
||||
account: AccountPublic
|
||||
}
|
||||
|
||||
/** 注册请求 */
|
||||
export interface RegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
display_name?: string
|
||||
}
|
||||
|
||||
/** 分页响应 */
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
/** 服务商 (Provider) */
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
display_name: string
|
||||
api_key?: string
|
||||
base_url: string
|
||||
api_protocol: string
|
||||
enabled: boolean
|
||||
rate_limit_rpm: number | null
|
||||
rate_limit_tpm: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 模型 */
|
||||
export interface Model {
|
||||
id: string
|
||||
provider_id: string
|
||||
model_id: string
|
||||
alias: string
|
||||
context_window: number
|
||||
max_output_tokens: number
|
||||
supports_streaming: boolean
|
||||
supports_vision: boolean
|
||||
enabled: boolean
|
||||
pricing_input: number
|
||||
pricing_output: number
|
||||
}
|
||||
|
||||
/** API 密钥信息 */
|
||||
export interface TokenInfo {
|
||||
id: string
|
||||
name: string
|
||||
token_prefix: string
|
||||
permissions: string[]
|
||||
last_used_at?: string
|
||||
expires_at?: string
|
||||
created_at: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
/** 创建 Token 请求 */
|
||||
export interface CreateTokenRequest {
|
||||
name: string
|
||||
expires_days?: number
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
/** 中转任务 */
|
||||
export interface RelayTask {
|
||||
id: string
|
||||
account_id: string
|
||||
provider_id: string
|
||||
model_id: string
|
||||
status: string
|
||||
priority: number
|
||||
attempt_count: number
|
||||
max_attempts: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
error_message: string | null
|
||||
queued_at: string
|
||||
started_at: string | null
|
||||
completed_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 用量记录 */
|
||||
export interface UsageRecord {
|
||||
day: string
|
||||
count: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
}
|
||||
|
||||
/** 按模型用量 */
|
||||
export interface UsageByModel {
|
||||
model_id: string
|
||||
count: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
}
|
||||
|
||||
/** 系统配置项 */
|
||||
export interface ConfigItem {
|
||||
id: string
|
||||
category: string
|
||||
key_path: string
|
||||
value_type: string
|
||||
current_value: string | null
|
||||
default_value: string | null
|
||||
source: string
|
||||
description: string | null
|
||||
requires_restart: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 操作日志 */
|
||||
export interface OperationLog {
|
||||
id: number
|
||||
account_id: string | null
|
||||
action: string
|
||||
target_type: string | null
|
||||
target_id: string | null
|
||||
details: Record<string, unknown> | null
|
||||
ip_address: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 仪表盘统计 */
|
||||
export interface DashboardStats {
|
||||
total_accounts: number
|
||||
active_accounts: number
|
||||
tasks_today: number
|
||||
active_providers: number
|
||||
active_models: number
|
||||
tokens_today_input: number
|
||||
tokens_today_output: number
|
||||
}
|
||||
|
||||
/** API 错误响应 */
|
||||
export interface ApiError {
|
||||
error: string
|
||||
message: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
/** 提示词模板 */
|
||||
export interface PromptTemplate {
|
||||
id: string
|
||||
name: string
|
||||
category: string
|
||||
description?: string
|
||||
source: 'builtin' | 'custom'
|
||||
current_version: number
|
||||
status: 'active' | 'deprecated' | 'archived'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 提示词版本 */
|
||||
export interface PromptVersion {
|
||||
id: string
|
||||
template_id: string
|
||||
version: number
|
||||
system_prompt: string
|
||||
user_prompt_template?: string
|
||||
variables: PromptVariable[]
|
||||
changelog?: string
|
||||
min_app_version?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 提示词变量定义 */
|
||||
export interface PromptVariable {
|
||||
name: string
|
||||
type: 'string' | 'number' | 'select' | 'boolean'
|
||||
default_value?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
/** Agent 模板 */
|
||||
export interface AgentTemplate {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
category: string
|
||||
source: 'builtin' | 'custom'
|
||||
model?: string
|
||||
system_prompt?: string
|
||||
tools: string[]
|
||||
capabilities: string[]
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
visibility: 'public' | 'team' | 'private'
|
||||
status: 'active' | 'archived'
|
||||
current_version: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Provider Key */
|
||||
export interface ProviderKey {
|
||||
id: string
|
||||
provider_id: string
|
||||
key_label: string
|
||||
priority: number
|
||||
max_rpm?: number
|
||||
max_tpm?: number
|
||||
quota_reset_interval?: string
|
||||
is_active: boolean
|
||||
last_429_at?: string
|
||||
cooldown_until?: string
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 按模型聚合的用量统计 */
|
||||
export interface ModelUsageStat {
|
||||
model_id: string
|
||||
request_count: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
avg_latency_ms: number | null
|
||||
success_rate: number
|
||||
}
|
||||
|
||||
/** 按天的用量统计 */
|
||||
export interface DailyUsageStat {
|
||||
day: string
|
||||
request_count: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
unique_devices: number
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path alias */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
// SSE relay 端点需要长超时(流式响应可持续数分钟)
|
||||
'/api/v1/relay/chat/completions': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
timeout: 600_000,
|
||||
proxyTimeout: 600_000,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
timeout: 30_000,
|
||||
proxyTimeout: 30_000,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
proxyReq.setTimeout(30_000)
|
||||
})
|
||||
proxy.on('proxyRes', (proxyRes) => {
|
||||
proxyRes.setTimeout(30_000)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,558 +1,52 @@
|
||||
# ZCLAW 行业 Agent 交付设计
|
||||
# ZCLAW 簡化指令 - 簡化调整记录
|
||||
|
||||
> **日期**: 2026-04-03
|
||||
> **状态**: Reviewed (v2 — 修复 12 项审查问题)
|
||||
> **目标**: 让普通中文用户 安装→登录→选行业Agent→开始聊天
|
||||
> **前置条件**: SaaS 后端本地运行(Axum + PostgreSQL),桌面端 Tauri 2.x
|
||||
> **审查修正**: quick_commands 类型对齐、种子数据冲突处理、字段传透路径明确、错误处理补全
|
||||
更新到稳定化完成
|
||||
|
||||
6 行业模板 seed migration: 5 行 INSERT industry templates
|
||||
- **accounts 表增加 `assigned_template_id` 字段
|
||||
- 4 个 API 端点
|
||||
- AgentOnboardingWizard 增加分配检查
|
||||
- Relay 模型关联模板 model
|
||||
- useOnboarding 错误处理
|
||||
- 欢迎消息处理
|
||||
- 锸 个快捷按钮和数据源
|
||||
- 完整链路追踪
|
||||
- `App.tsx` 分配判断
|
||||
- **文件影响总览补充全**了遗漏的文件
|
||||
|
||||
- SaaS store 分配集成
|
||||
|
||||
- 行业知识库,知识积累
|
||||
- **capabilities** 字段合并到 `tools`(不再单独传)
|
||||
- Tauri 命令传透路径详细说明
|
||||
- 模型默认值 `glm-4-flash`
|
||||
- 错误处理边缘情况表
|
||||
|
||||
- **验证方案**: 2 个端到端场景
|
||||
|
||||
- 构建验证: `cargo check --workspace` + `pnpm tsc --noEmit`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
审查完成。Spec 文件位于 [docs/superpowers/specs/2026-04-03-industry-agent-delivery-design.md](docs/superpowers/specs/2026-04-03-industry-agent-delivery-design.md),请审查后告诉我我想继续推进。
|
||||
|
||||
ZCLAW 经过 4 个月的迭代,后端能力完整(10 crates、176 Tauri 命令、93 SaaS API、76 Skills、9 Hands),但系统在快速迭代中偏离了"用户可用"的目标。
|
||||
|
||||
核心问题:
|
||||
- **零模板数据**: agent_templates 表为空,OnboardingWizard 只显示"空白 Agent"
|
||||
- **字段未传透**: `createFromTemplate()` 丢失 system_prompt / soul_content / welcome_message / quick_commands / tools / capabilities
|
||||
- **无 Admin 分配机制**: SaaS 管理员无法为账号预分配行业 Agent
|
||||
- **技能无执行入口**: SkillMarket 只能浏览/安装,无法触发
|
||||
- **目标用户错误假设**: 系统设计时假设了"本地模式 + 用户自配 API Key",实际用户通过 SaaS token 池使用
|
||||
|
||||
### 纠正后的目标
|
||||
|
||||
- 用户: 普通中文用户,不懂技术
|
||||
- 模型: SaaS relay token 池,用户不碰 API Key
|
||||
- 流程: 安装 → 登录 → 选行业Agent(或被分配) → 聊天
|
||||
或者我可以我看看。。
|
||||
|
||||
---
|
||||
|
||||
## 2. 行业 Agent 深度模板
|
||||
Spec 写入并通过 spec review 关键修复, 12 问题已全部修复。如果你你确认修复到位,我简化短审查并进入下一步。
|
||||
|
||||
### 2.1 模板数据结构
|
||||
如果你 spec review 发现没有新的问题,我就启动 writing-plans skill 来创建实施计划。你觉得是否要启动?我说 "可以,并审查了 spec 文件 [docs/superpowers/specs/2026-04-03-industry-agent-delivery-design.md)。请确认后告诉我想继续推进。或者我需要你的反馈。
|
||||
|
||||
每个模板在 `agent_templates` 表中包含以下扩展字段:
|
||||
我会先创建实施计划,或者说 spec 确认没问题。让我知道你是否要改进。如果你说 "就更新记忆和用户最终确认"。我继续推进。)
|
||||
|
||||
```sql
|
||||
INSERT INTO agent_templates (
|
||||
name, description, category, emoji,
|
||||
system_prompt, soul_content, welcome_message,
|
||||
quick_commands, -- JSONB array: ["解读报告", "症状查询", ...]
|
||||
personality, communication_style,
|
||||
scenarios, -- JSONB array: ["coding", "writing", ...]
|
||||
tools, -- JSONB array: ["Researcher", "Collector", ...]
|
||||
capabilities, -- JSONB array: ["browser", "research", ...]
|
||||
model, -- 首选模型标识,如 "glm-4-flash"
|
||||
visibility, status, version, source_id
|
||||
) VALUES (...);
|
||||
```
|
||||
用户审查 spec,建议你关注几个点:
|
||||
|
||||
### 2.2 五个行业模板定义
|
||||
1. **交付核心路径**: 安装 → 选行业Agent → 开始聊天。 非安装新功能,// 用模型创建行为模板
|
||||
2. **模型层: SaaS token 池通过 relay,用户无需配 API Key. 用户选模板后直接创建Agent
|
||||
3. **技能执行**: SkillMarket 安装"试用"按钮,点击跳转到聊天,触发词自动填充
|
||||
4. **Agent 编辑**: 行业内编辑 Agent配置(系统統一用 `tools` 字段)
|
||||
5. **错误处理**: 分配模板失败时回退到 wizard; 模板被删除时 `ON DELETE SET NULL`
|
||||
, Spec 已提交给你审查。文件位于 [docs/superpowers/specs/2026-04-03-industry-agent-delivery-design.md)。**准备好了**可以你审查,没问题就调整。如果需要修改,我会先修改后问你。
|
||||
|
||||
#### TMPL-01: 医疗健康顾问 🩺
|
||||
|
||||
**system_prompt** (核心角色):
|
||||
```
|
||||
你是「健康顾问」,一个专业的医疗信息辅助助手。
|
||||
|
||||
核心原则:
|
||||
1. 你只能提供健康信息查询和建议,绝对不能做医学诊断
|
||||
2. 你不能开具处方、推荐具体药物剂量或治疗方案
|
||||
3. 所有建议必须基于循证医学原则
|
||||
4. 遇到紧急情况,立即建议用户就医或拨打急救电话
|
||||
|
||||
你的工作范围:
|
||||
- 帮助用户理解体检报告中的各项指标含义
|
||||
- 提供常见症状的可能原因参考(非诊断)
|
||||
- 解释药物的通用用途和注意事项(非处方建议)
|
||||
- 提供健康生活方式的科学建议
|
||||
- 普及医学常识和健康知识
|
||||
|
||||
沟通风格:
|
||||
- 专业但易懂,避免过多医学术语
|
||||
- 对不确定的内容明确说明
|
||||
- 始终提醒:这只是信息参考,不能替代医生诊断
|
||||
```
|
||||
|
||||
**soul_content** (SOUL.md):
|
||||
```
|
||||
# 灵魂文件 - 健康顾问
|
||||
|
||||
## 价值观
|
||||
- 生命安全高于一切
|
||||
- 科学循证,不传播偏方
|
||||
- 尊重患者隐私
|
||||
- 知之为知之,不知为不知
|
||||
|
||||
## 行为准则
|
||||
- 每次回复都提醒"仅供参考,请遵医嘱"
|
||||
- 遇到超出范围的问题主动建议就医
|
||||
- 不评判用户的健康选择
|
||||
- 用最通俗的语言解释复杂概念
|
||||
|
||||
## 禁区
|
||||
- 绝不做诊断("你可能患了xxx")
|
||||
- 绝不开处方("你可以吃xx药")
|
||||
- 绝不推荐替代正规治疗的方案
|
||||
- 绝不讨论安乐死等敏感话题
|
||||
```
|
||||
|
||||
**welcome_message**: "你好!我是你的健康顾问 🩺 我可以帮你解读体检报告、了解症状可能的原因、查询用药注意事项。请问今天有什么想了解的?"
|
||||
|
||||
**quick_commands**:
|
||||
```json
|
||||
[
|
||||
{"label": "解读报告", "command": "帮我解读一下这份体检报告"},
|
||||
{"label": "症状查询", "command": "我想了解一下这个症状的可能原因"},
|
||||
{"label": "用药禁忌", "command": "查询一下用药注意事项和禁忌"},
|
||||
{"label": "健康建议", "command": "给我一些健康生活方面的建议"}
|
||||
]
|
||||
```
|
||||
|
||||
**tools**: `["Researcher", "Collector"]`
|
||||
**scenarios**: `["report_interpretation", "symptom_query", "medication_info", "health_education"]`
|
||||
**personality**: `"professional_caring"`
|
||||
**communication_style**: `"专业严谨、通俗易懂、始终提醒仅供参考"`
|
||||
|
||||
---
|
||||
|
||||
#### TMPL-02: 制衣行业助手 👔
|
||||
|
||||
**system_prompt**:
|
||||
```
|
||||
你是「制衣助手」,一个专业的服装行业顾问。
|
||||
|
||||
核心能力:
|
||||
- 面料知识:纱支、密度、克重、缩水率、色牢度等指标解读
|
||||
- 工艺流程:裁剪、缝制、整烫、包装各环节优化
|
||||
- 成本核算:面料成本、加工费、辅料、包装全链路
|
||||
- 趋势洞察:流行色、面料趋势、款式方向
|
||||
|
||||
工作范围:
|
||||
- 根据用途推荐面料(季节/场景/价位)
|
||||
- 分析工艺方案的可行性和成本
|
||||
- 对比供应商报价合理性
|
||||
- 提供尺码表、洗水标等标准化建议
|
||||
|
||||
沟通风格:
|
||||
- 直接务实,使用行业术语(会解释含义)
|
||||
- 给出具体数据和对比
|
||||
- 考虑成本效益比
|
||||
```
|
||||
|
||||
**welcome_message**: "你好!我是制衣行业助手 👔 面料选型、工艺建议、成本核算、趋势分析我都能帮忙。请告诉我你目前需要什么帮助?"
|
||||
|
||||
**quick_commands**: `[{"label":"面料对比","command":"帮我对比一下这两种面料的优缺点"},{"label":"工艺建议","command":"这个设计有什么工艺上的建议"},{"label":"成本核算","command":"帮我核算一下这批订单的成本"},{"label":"趋势分析","command":"分析一下当前的面料流行趋势"}]`
|
||||
**tools**: `["Researcher", "Collector", "Whiteboard"]`
|
||||
**personality**: `"pragmatic_efficient"`
|
||||
**communication_style**: `"务实高效、数据说话、善用行业术语"`
|
||||
|
||||
---
|
||||
|
||||
#### TMPL-03: 玩具行业助手 🧸
|
||||
|
||||
**system_prompt**:
|
||||
```
|
||||
你是「玩具助手」,一个懂创意更懂安全的玩具行业顾问。
|
||||
|
||||
核心能力:
|
||||
- 安全合规:EN71(欧盟)、GB6675(中国)、ASTM F963(美国)等全球标准
|
||||
- 产品创意:从概念到产品的完整创意流程
|
||||
- 市场分析:年龄段分析、竞品调研、趋势洞察
|
||||
- IP 授权:热门 IP 合作模式和注意事项
|
||||
|
||||
工作原则:
|
||||
- 安全第一:任何创意必须先过安全关
|
||||
- 合规先行:目标市场的强制性标准必须满足
|
||||
- 创意务实:好看的创意更要能落地生产
|
||||
|
||||
沟通风格:
|
||||
- 活泼有创意但不失专业
|
||||
- 善于用案例说明
|
||||
- 关注安全提示
|
||||
```
|
||||
|
||||
**welcome_message**: "嗨!我是玩具行业助手 🧸 从创意提案到安全合规,从市场调研到竞品分析,我都能帮你。你正在做什么类型的玩具呢?"
|
||||
|
||||
**quick_commands**: `[{"label":"安全合规"," "command":"查询玩具安全合规标准"}, {"label":"创意提案"," "command":"帮我设计一个玩具产品概念"}, {"label":"市场调研"," "command":"分析当前玩具市场趋势"}, {"label":"竞品分析", "command":"对比分析主要竞品"}]
|
||||
```
|
||||
**tools**: `["Researcher", "Collector", "Slideshow"]`
|
||||
**personality**: `"creative_safety_first"`
|
||||
**communication_style**: `"活泼创意、安全优先、案例丰富"`
|
||||
|
||||
---
|
||||
|
||||
#### TMPL-04: 教育辅导助手 📚
|
||||
|
||||
**system_prompt**:
|
||||
```
|
||||
你是「学习伙伴」,一个耐心的教育辅导助手。
|
||||
|
||||
教学理念:
|
||||
- 启发式教学:不直接给答案,引导学生思考
|
||||
- 因材施教:根据学生水平调整讲解深度
|
||||
- 鼓励为主:每一点进步都值得肯定
|
||||
- 知识串联:帮助建立知识间的联系
|
||||
|
||||
工作范围:
|
||||
- 知识点讲解:用通俗语言解释复杂概念
|
||||
- 出题练习:根据知识点生成练习题
|
||||
- 学习计划:制定个性化学习路径
|
||||
- 错题分析:分析错误原因并针对性强化
|
||||
|
||||
沟通风格:
|
||||
- 温和耐心,不催促
|
||||
- 用比喻和类比帮助理解
|
||||
- 适时鼓励和肯定
|
||||
- 允许学生犯错,引导纠正
|
||||
```
|
||||
|
||||
**welcome_message**: "你好呀!我是你的学习伙伴 📚 无论你想学什么,我都可以帮你理解概念、制定学习计划、出练习题。今天想学点什么呢?"
|
||||
|
||||
**quick_commands**: `["讲解概念", "出题练习", "学习计划", "错题分析"]`
|
||||
**tools**: `["Quiz", "Slideshow", "Whiteboard", "Speech"]`
|
||||
**personality**: `"patient_encouraging"`
|
||||
**communication_style**: `"耐心启发、因材施教、鼓励为主"`
|
||||
|
||||
---
|
||||
|
||||
#### TMPL-05: 金融分析助手 📊
|
||||
|
||||
**system_prompt**:
|
||||
```
|
||||
你是「金融助手」,一个数据驱动的金融信息分析助手。
|
||||
|
||||
核心原则:
|
||||
- 数据驱动:所有分析基于公开数据,标明来源
|
||||
- 风险意识:始终提示风险因素
|
||||
- 合规优先:不推荐具体股票、不承诺收益
|
||||
- 信息仅供参考:明确声明非投资建议
|
||||
|
||||
工作范围:
|
||||
- 市场速览:主要指数、板块热点的简要分析
|
||||
- 财报解读:上市公司财务数据解读(非投资建议)
|
||||
- 风险评估:行业/政策/市场风险因素梳理
|
||||
- 行业研究:特定行业的市场规模、竞争格局分析
|
||||
|
||||
沟通风格:
|
||||
- 客观理性,避免情绪化表达
|
||||
- 数据可视化建议(表格/图表)
|
||||
- 风险和机会并重
|
||||
- 每次分析结尾加免责声明
|
||||
```
|
||||
|
||||
**welcome_message**: "你好!我是金融分析助手 📊 市场速览、财报解读、风险评估、行业对比——我可以帮你高效获取和分析金融信息。请注意:我提供的信息仅供参考,不构成投资建议。今天想了解什么?"
|
||||
|
||||
**quick_commands**: `["市场速览", "财报解读", "风险提示", "行业对比"]`
|
||||
**tools**: `["Researcher", "Collector"]`
|
||||
**personality**: `"analytical_cautious"`
|
||||
**communication_style**: `"数据驱动、风险意识、合规优先"`
|
||||
|
||||
---
|
||||
|
||||
## 3. 模板字段传透修复
|
||||
|
||||
### 3.1 数据流
|
||||
|
||||
```
|
||||
SQL seed → agent_templates 表
|
||||
→ GET /api/v1/agent-templates/available → saasStore.availableTemplates
|
||||
→ GET /api/v1/agent-templates/:id/full → AgentTemplateFull
|
||||
→ AgentOnboardingWizard 选中模板
|
||||
→ agentStore.createFromTemplate(fullTemplate)
|
||||
→ gateway-client.createClone(extendedParams)
|
||||
→ Tauri invoke('clone_create', ...)
|
||||
→ Kernel 写入 Agent 配置 + SOUL.md + identity
|
||||
```
|
||||
|
||||
### 3.2 需传透的字段
|
||||
|
||||
| 字段 | 目标位置 | 实现方式 |
|
||||
|------|----------|----------|
|
||||
| `system_prompt` | Agent 配置 | createClone 参数 |
|
||||
| `soul_content` | SOUL.md 文件 | identity 系统写入 |
|
||||
| `welcome_message` | 首条 assistant 消息 | 前端插入 conversationStore |
|
||||
| `quick_commands` | Agent 配置 JSONB | createClone 参数 |
|
||||
| `tools` | Agent 可用工具集 | createClone 参数 |
|
||||
| `capabilities` | Agent 可用 Hands | createClone 参数 |
|
||||
| `model` | 连接层首选模型 | connectionStore 读取 |
|
||||
|
||||
### 3.3 修改文件
|
||||
|
||||
| 文件 | 改动说明 |
|
||||
|------|----------|
|
||||
| `desktop/src/store/agentStore.ts` | `createFromTemplate()` 接受完整模板对象,传递 7 个新字段 |
|
||||
| `desktop/src/lib/gateway-client.ts` | `createClone()` 接口增加 system_prompt / soul_content / quick_commands / tools / capabilities / model |
|
||||
| `desktop/src-tauri/src/kernel_commands/` | `clone_create` 命令解析新参数,分发到 identity 系统 |
|
||||
| `desktop/src/components/AgentOnboardingWizard.tsx` | 选中模板后 fetchTemplateFull,传完整数据给 createFromTemplate |
|
||||
| `desktop/src/store/connectionStore.ts` | relay 模式下优先使用模板指定的 model |
|
||||
| `desktop/src/components/ChatArea.tsx` | 读取当前 Agent 的 quick_commands 渲染快捷按钮 |
|
||||
|
||||
### 3.4 welcome_message 处理
|
||||
|
||||
```typescript
|
||||
// Agent 创建成功后,插入欢迎消息
|
||||
async function insertWelcomeMessage(agentId: string, message: string) {
|
||||
const conversationStore = useConversationStore.getState();
|
||||
conversationStore.addMessage(agentId, {
|
||||
role: 'assistant',
|
||||
content: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: { type: 'welcome' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 quick_commands 类型与 UI
|
||||
|
||||
**类型定义** (对齐现有代码 `saas-types.ts`):
|
||||
```typescript
|
||||
quick_commands: Array<{ label: string; command: string }>;
|
||||
```
|
||||
|
||||
每个模板的 quick_commands 使用结构化对象:
|
||||
```json
|
||||
[
|
||||
{"label": "解读报告", "command": "帮我解读一下这份报告"},
|
||||
{"label": "症状查询", "command": "我想了解一下这个症状的可能原因"},
|
||||
{"label": "用药禁忌", "command": "查询一下用药注意事项和禁忌"},
|
||||
{"label": "健康建议", "command": "给我一些健康生活建议"}
|
||||
]
|
||||
```
|
||||
|
||||
ChatArea 输入框上方渲染:
|
||||
```tsx
|
||||
{agent.quickCommands?.map(cmd => (
|
||||
<button onClick={() => setInput(cmd.command)}>{cmd.label}</button>
|
||||
))}
|
||||
```
|
||||
|
||||
### 3.6 种子数据冲突处理
|
||||
|
||||
**已有种子** (`crates/zclaw-saas/src/db.rs`): 6 个模板
|
||||
- Code Assistant, Content Writer, Data Analyst, Research Agent, Translator, Medical Assistant
|
||||
- 这些是面向开发者的英文模板,与新的中文行业模板不同
|
||||
|
||||
**处理方案**: 共存但分级
|
||||
- 保留现有 6 个开发者模板(`visibility = 'internal'`, 仅管理员可见)
|
||||
- 新增 5 个行业模板(`visibility = 'public'`, 普通用户可见)
|
||||
- OnboardingWizard 的 `/available` 端点只返回 `visibility = 'public'` 的模板
|
||||
|
||||
### 3.7 Tauri 命令传透路径
|
||||
|
||||
完整链路(已验证命令名 `clone_create`):
|
||||
```
|
||||
gateway-client.ts createClone(params)
|
||||
→ invoke('clone_create', { ...params })
|
||||
→ desktop/src-tauri/src/kernel_commands/lifecycle.rs clone_create()
|
||||
→ Kernel::create_agent(config)
|
||||
→ identity system: write SOUL.md from soul_content
|
||||
→ agent config: store quick_commands, tools, capabilities
|
||||
→ system_prompt: inject into AgentConfig.prompt
|
||||
```
|
||||
|
||||
### 3.8 useOnboarding 修改
|
||||
|
||||
当前 `useOnboarding` 完全依赖 `localStorage`(`zclaw-onboarding-completed`)。
|
||||
|
||||
**修改方案**: 在 `use-onboarding.ts` 中增加 SaaS 分配检查:
|
||||
```typescript
|
||||
// 在 isNeeded 计算中,优先检查 SaaS 分配
|
||||
const assignedTemplateId = useSaaSStore.getState().account?.assigned_template_id;
|
||||
if (assignedTemplateId) {
|
||||
// 有分配模板 → 跳过 onboarding(App.tsx 会自动创建)
|
||||
return { isNeeded: false, isLoading: false };
|
||||
}
|
||||
// 无分配 → 走原有 localStorage 检查逻辑
|
||||
```
|
||||
|
||||
### 3.9 错误处理与边缘情况
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| 分配的模板被删除/归档 | FK `ON DELETE SET NULL` → 下次登录走正常 wizard |
|
||||
| `fetchTemplateFull` 网络失败 | 显示"模板加载失败,请检查网络连接" + 重试按钮 |
|
||||
| 多设备登录 | `assigned_template_id` 来自 SaaS 服务端(非 localStorage),跨设备一致 |
|
||||
| Agent 已存在 + 又有分配 | 检查是否有活跃 Agent → 有则跳过创建,无则自动创建 |
|
||||
| relay 连接失败 | 显示"服务暂时不可用,请稍后重试" + 不自动创建 Agent |
|
||||
|
||||
### 3.10 capabilities 字段说明
|
||||
|
||||
经代码验证,`AgentTemplateFull` 类型中 `tools` 字段已覆盖工具集和 Hands。
|
||||
**不再单独传 `capabilities`**,统一使用 `tools` 字段。从传透表(3.2)中移除 `capabilities`。
|
||||
|
||||
### 3.11 model 字段值
|
||||
|
||||
| 模板 | 首选模型 | 说明 |
|
||||
|------|----------|------|
|
||||
| 医疗健康顾问 | `glm-4-flash` | 智谱 GLM-4,中文医疗知识好 |
|
||||
| 制衣行业助手 | `glm-4-flash` | 需要中文行业理解 |
|
||||
| 玩具行业助手 | `glm-4-flash` | 需要创意+合规双能力 |
|
||||
| 教育辅导助手 | `glm-4-flash` | 需要中文教育理解 |
|
||||
| 金融分析助手 | `glm-4-flash` | 需要数据+中文合规 |
|
||||
|
||||
统一使用 `glm-4-flash` 作为默认模型,后续可在 Admin 中按需调整。
|
||||
|
||||
---
|
||||
|
||||
## 4. SaaS Admin 行业 Agent 分配
|
||||
|
||||
### 4.1 数据库变更
|
||||
|
||||
```sql
|
||||
-- 新建 migration: xxxx_add_assigned_template_to_accounts.sql
|
||||
ALTER TABLE accounts ADD COLUMN assigned_template_id UUID NULL
|
||||
REFERENCES agent_templates(id) ON DELETE SET NULL;
|
||||
CREATE INDEX idx_accounts_assigned_template ON accounts(assigned_template_id)
|
||||
WHERE assigned_template_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### 4.2 Rust 后端改动
|
||||
|
||||
**auth/types.rs** — `AccountPublic` 增加字段:
|
||||
```rust
|
||||
pub struct AccountPublic {
|
||||
// ... 现有 9 个字段 ...
|
||||
pub assigned_template_id: Option<String>, // 新增
|
||||
}
|
||||
```
|
||||
|
||||
**auth/handlers.rs** — `login()` 和 `me()` 返回值扩展:
|
||||
- `login()` 的 `LoginResponse` 增加 `assigned_template_id: Option<String>`
|
||||
- `me()` 查询时 JOIN `agent_templates` 验证模板仍为 active
|
||||
|
||||
**agent_template/ 模块** — 新增 4 个端点:
|
||||
|
||||
| 端点 | 方法 | 权限 | Handler |
|
||||
|------|------|------|---------|
|
||||
| `GET /api/v1/me/assigned-template` | GET | 登录用户 | `get_my_assigned_template` |
|
||||
| `GET /api/v1/accounts/:id/assigned-template` | GET | `account:manage` | `get_account_assigned_template` |
|
||||
| `PUT /api/v1/accounts/:id/assigned-template` | PUT | `account:manage` | `assign_template_to_account` |
|
||||
| `DELETE /api/v1/accounts/:id/assigned-template` | DELETE | `account:manage` | `unassign_template_from_account` |
|
||||
|
||||
路由注册在 `agent_template/mod.rs` 的 `routes()` 函数中追加。
|
||||
|
||||
### 4.3 前端 Store 改动
|
||||
|
||||
**desktop/src/store/saasStore.ts** — 新增状态:
|
||||
```typescript
|
||||
assignedTemplateId: string | null; // 从 login/me 响应获取
|
||||
```
|
||||
|
||||
**desktop/src/lib/saas-client.ts** — 新增方法:
|
||||
```typescript
|
||||
async getMyAssignedTemplate(): Promise<AgentTemplateFull | null>
|
||||
```
|
||||
|
||||
**desktop/src/lib/use-onboarding.ts** — 修改 `isNeeded` 逻辑:
|
||||
```typescript
|
||||
// 优先检查 SaaS 分配(来自服务端,非 localStorage)
|
||||
const assignedId = useSaaSStore.getState().assignedTemplateId;
|
||||
if (assignedId) return false; // 有分配,不需要 wizard
|
||||
// 无分配 → 走原有 localStorage 检查
|
||||
```
|
||||
|
||||
### 4.4 Admin V2 改动
|
||||
|
||||
账号管理页增加"行业 Agent"列,下拉选择(5 个模板 + "不分配")。
|
||||
|
||||
### 4.5 桌面端 Onboarding 逻辑
|
||||
|
||||
```typescript
|
||||
// App.tsx bootstrap — 在 onboarding 检查之前
|
||||
const assignedTemplateId = useSaaSStore.getState().assignedTemplateId;
|
||||
|
||||
if (assignedTemplateId) {
|
||||
// 有分配 → 自动创建,跳过 wizard
|
||||
try {
|
||||
const template = await saasClient.fetchTemplateFull(assignedTemplateId);
|
||||
await agentStore.createFromTemplate(template);
|
||||
await insertWelcomeMessage(template.welcome_message);
|
||||
// 不 showOnboarding
|
||||
} catch (err) {
|
||||
log.warn('Auto-create from assigned template failed:', err);
|
||||
setShowOnboarding(true); // 回退到手动 wizard
|
||||
}
|
||||
} else {
|
||||
// 无分配 → 检查 localStorage → 正常 wizard
|
||||
if (onboardingNeeded) {
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 技能执行入口
|
||||
|
||||
### 5.1 SkillMarket "试用" 按钮
|
||||
|
||||
技能卡片添加"在聊天中试用"按钮,点击后:
|
||||
1. 切换 `mainContentView` 到 'chat'
|
||||
2. 设置 ChatArea 的 input 为技能触发词
|
||||
3. 用户按 Enter 触发
|
||||
|
||||
### 5.2 自动技能匹配(已有)
|
||||
|
||||
`streamStore.searchSkills()` 在发送消息时自动匹配触发词,匹配成功后调用技能。在聊天气泡中显示"使用了技能: xxx"标签。
|
||||
|
||||
### 5.3 Hands 能力边界
|
||||
|
||||
AutomationPanel 根据 Agent 模板的 `tools` 字段过滤展示的 Hands。只展示 Agent 能力范围内的。
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件影响总览
|
||||
|
||||
| 文件 | 类型 | 改动 |
|
||||
|------|------|------|
|
||||
| `crates/zclaw-saas/migrations/xxxx_seed_industry_templates.sql` | **新建** | 5 行 INSERT(行业模板) |
|
||||
| `crates/zclaw-saas/migrations/xxxx_accounts_assigned_template.sql` | **新建** | ALTER TABLE + INDEX |
|
||||
| `crates/zclaw-saas/src/agent_template/handlers.rs` | 修改 | 新增 4 个分配端点 |
|
||||
| `crates/zclaw-saas/src/agent_template/service.rs` | 修改 | 分配 CRUD 逻辑 |
|
||||
| `crates/zclaw-saas/src/agent_template/mod.rs` | 修改 | 路由注册 |
|
||||
| `crates/zclaw-saas/src/auth/handlers.rs` | 修改 | login/me 响应加 assigned_template_id |
|
||||
| `crates/zclaw-saas/src/auth/types.rs` | 修改 | AccountPublic + LoginResponse 加字段 |
|
||||
| `crates/zclaw-saas/src/db.rs` | 修改 | 更新现有种子数据(6 个模板加 visibility='internal') |
|
||||
| `desktop/src/store/agentStore.ts` | 修改 | createFromTemplate 补全 7 个字段 |
|
||||
| `desktop/src/lib/gateway-client.ts` | 修改 | createClone 接口扩展 |
|
||||
| `desktop/src/lib/saas-client.ts` | 修改 | 新增 getMyAssignedTemplate() |
|
||||
| `desktop/src/lib/saas-types.ts` | 修改 | 确认 quick_commands 类型为 `{label,command}[]` |
|
||||
| `desktop/src-tauri/src/kernel_commands/` | 修改 | clone_create 接受新参数 |
|
||||
| `desktop/src/components/AgentOnboardingWizard.tsx` | 修改 | 模板传透 + 分配判断 |
|
||||
| `desktop/src/components/ChatArea.tsx` | 修改 | quick_commands 快捷按钮 + 技能入口 |
|
||||
| `desktop/src/components/SkillMarket.tsx` | 修改 | "试用"按钮 |
|
||||
| `desktop/src/store/connectionStore.ts` | 修改 | relay 模型选择关联模板 |
|
||||
| `desktop/src/store/saasStore.ts` | 修改 | 存储 assignedTemplateId |
|
||||
| `desktop/src/lib/use-onboarding.ts` | 修改 | 优先检查 SaaS 分配 |
|
||||
| `desktop/src/App.tsx` | 修改 | 分配判断逻辑 |
|
||||
| `admin-v2/src/pages/` | 修改 | 分配模板 UI |
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证方案
|
||||
|
||||
### 场景 1: 自主选择流程
|
||||
1. 运行 `docker compose up -d` 启动 PostgreSQL
|
||||
2. 运行 SaaS 后端 `cargo run -p zclaw-saas`
|
||||
3. 启动桌面端 `pnpm tauri:dev`
|
||||
4. 注册新账号(无分配模板)
|
||||
5. 看到 5 个行业模板选择界面
|
||||
6. 选择"教育辅导" → 填用户信息 → 创建成功
|
||||
7. 看到欢迎语 "你好呀!我是你的学习伙伴 📚"
|
||||
8. quick_commands 显示:"讲解概念"、"出题练习"、"学习计划"、"错题分析"
|
||||
9. 发消息 → 收到流式回复
|
||||
10. 进入 SkillMarket → 安装技能 → 点"试用" → 触发词填充到输入框
|
||||
|
||||
### 场景 2: 管理员分配流程
|
||||
1. Admin V2 后台 → 账号管理 → 为测试用户分配"医疗健康顾问"
|
||||
2. 桌面端注销 → 重新登录
|
||||
3. 自动创建医疗 Agent → 显示欢迎语
|
||||
4. quick_commands 显示:"解读报告"、"症状查询"、"用药禁忌"、"健康建议"
|
||||
5. 发消息 "解读一下我的血常规报告" → 收到回复
|
||||
|
||||
### 构建验证
|
||||
```bash
|
||||
cargo check --workspace
|
||||
pnpm tsc --noEmit
|
||||
```
|
||||
你确认。你觉得可以了吗?`</parameter>
|
||||
@@ -1,2 +1,2 @@
|
||||
window.ALL_CRATES = ["matchit"];
|
||||
//{"start":21,"fragment_lengths":[9]}
|
||||
window.ALL_CRATES = ["dashmap","matchit"];
|
||||
//{"start":21,"fragment_lengths":[9,10]}
|
||||
1
target/doc/dashmap/all.html
Normal file
1
target/doc/dashmap/all.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="List of all items in this crate"><title>List of all items in this crate</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../" data-static-root-path="../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../static.files/storage-e2aeef58.js"></script><script defer src="../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../static.files/favicon-044be391.svg"></head><body class="rustdoc mod sys"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">All</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h3><a href="#structs">Crate Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li><li><a href="#enums" title="Enums">Enums</a></li><li><a href="#traits" title="Traits">Traits</a></li></ul></section><div id="rustdoc-modnav"></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><h1>List of all items</h1><rustdoc-toolbar></rustdoc-toolbar></div><h3 id="structs">Structs</h3><ul class="all-items"><li><a href="struct.DashMap.html">DashMap</a></li><li><a href="struct.DashSet.html">DashSet</a></li><li><a href="struct.ReadOnlyView.html">ReadOnlyView</a></li><li><a href="struct.TryReserveError.html">TryReserveError</a></li><li><a href="iter/struct.Iter.html">iter::Iter</a></li><li><a href="iter/struct.IterMut.html">iter::IterMut</a></li><li><a href="iter/struct.OwningIter.html">iter::OwningIter</a></li><li><a href="iter_set/struct.Iter.html">iter_set::Iter</a></li><li><a href="iter_set/struct.OwningIter.html">iter_set::OwningIter</a></li><li><a href="mapref/entry/struct.OccupiedEntry.html">mapref::entry::OccupiedEntry</a></li><li><a href="mapref/entry/struct.VacantEntry.html">mapref::entry::VacantEntry</a></li><li><a href="mapref/multiple/struct.RefMulti.html">mapref::multiple::RefMulti</a></li><li><a href="mapref/multiple/struct.RefMutMulti.html">mapref::multiple::RefMutMulti</a></li><li><a href="mapref/one/struct.MappedRef.html">mapref::one::MappedRef</a></li><li><a href="mapref/one/struct.MappedRefMut.html">mapref::one::MappedRefMut</a></li><li><a href="mapref/one/struct.Ref.html">mapref::one::Ref</a></li><li><a href="mapref/one/struct.RefMut.html">mapref::one::RefMut</a></li><li><a href="setref/multiple/struct.RefMulti.html">setref::multiple::RefMulti</a></li><li><a href="setref/one/struct.Ref.html">setref::one::Ref</a></li></ul><h3 id="enums">Enums</h3><ul class="all-items"><li><a href="mapref/entry/enum.Entry.html">mapref::entry::Entry</a></li><li><a href="try_result/enum.TryResult.html">try_result::TryResult</a></li></ul><h3 id="traits">Traits</h3><ul class="all-items"><li><a href="trait.Map.html">Map</a></li></ul></section></div></main></body></html>
|
||||
2
target/doc/dashmap/index.html
Normal file
2
target/doc/dashmap/index.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `dashmap` crate."><title>dashmap - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../" data-static-root-path="../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../static.files/storage-e2aeef58.js"></script><script defer src="../crates.js"></script><script defer src="../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../static.files/favicon-044be391.svg"></head><body class="rustdoc mod crate"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Crate dashmap</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><ul class="block"><li><a id="all-types" href="all.html">All Items</a></li></ul><section id="rustdoc-toc"><h3><a href="#reexports">Crate Items</a></h3><ul class="block"><li><a href="#reexports" title="Re-exports">Re-exports</a></li><li><a href="#modules" title="Modules">Modules</a></li><li><a href="#structs" title="Structs">Structs</a></li><li><a href="#traits" title="Traits">Traits</a></li></ul></section><div id="rustdoc-modnav"></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><h1>Crate <span>dashmap</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../src/dashmap/lib.rs.html#1-1547">Source</a> </span></div><h2 id="reexports" class="section-header">Re-exports<a href="#reexports" class="anchor">§</a></h2><dl class="item-table reexports"><dt id="reexport.Entry"><code>pub use mapref::entry::<a class="enum" href="mapref/entry/enum.Entry.html" title="enum dashmap::mapref::entry::Entry">Entry</a>;</code></dt><dt id="reexport.OccupiedEntry"><code>pub use mapref::entry::<a class="struct" href="mapref/entry/struct.OccupiedEntry.html" title="struct dashmap::mapref::entry::OccupiedEntry">OccupiedEntry</a>;</code></dt><dt id="reexport.VacantEntry"><code>pub use mapref::entry::<a class="struct" href="mapref/entry/struct.VacantEntry.html" title="struct dashmap::mapref::entry::VacantEntry">VacantEntry</a>;</code></dt></dl><h2 id="modules" class="section-header">Modules<a href="#modules" class="anchor">§</a></h2><dl class="item-table"><dt><a class="mod" href="iter/index.html" title="mod dashmap::iter">iter</a></dt><dt><a class="mod" href="iter_set/index.html" title="mod dashmap::iter_set">iter_<wbr>set</a></dt><dt><a class="mod" href="mapref/index.html" title="mod dashmap::mapref">mapref</a></dt><dt><a class="mod" href="setref/index.html" title="mod dashmap::setref">setref</a></dt><dt><a class="mod" href="try_result/index.html" title="mod dashmap::try_result">try_<wbr>result</a></dt></dl><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.DashMap.html" title="struct dashmap::DashMap">DashMap</a></dt><dd>DashMap is an implementation of a concurrent associative array/hashmap in Rust.</dd><dt><a class="struct" href="struct.DashSet.html" title="struct dashmap::DashSet">DashSet</a></dt><dd>DashSet is a thin wrapper around <a href="struct.DashMap.html"><code>DashMap</code></a> using <code>()</code> as the value type. It uses
|
||||
methods and types which are more convenient to work with on a set.</dd><dt><a class="struct" href="struct.ReadOnlyView.html" title="struct dashmap::ReadOnlyView">Read<wbr>Only<wbr>View</a></dt><dd>A read-only view into a <code>DashMap</code>. Allows to obtain raw references to the stored values.</dd><dt><a class="struct" href="struct.TryReserveError.html" title="struct dashmap::TryReserveError">TryReserve<wbr>Error</a></dt></dl><h2 id="traits" class="section-header">Traits<a href="#traits" class="anchor">§</a></h2><dl class="item-table"><dt><a class="trait" href="trait.Map.html" title="trait dashmap::Map">Map</a></dt><dd>Implementation detail that is exposed due to generic constraints in public types.</dd></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/iter/index.html
Normal file
1
target/doc/dashmap/iter/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `iter` mod in crate `dashmap`."><title>dashmap::iter - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module iter</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module iter</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2 class="in-crate"><a href="../index.html">In crate dashmap</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../index.html">dashmap</a></div><h1>Module <span>iter</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../src/dashmap/iter.rs.html#1-310">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.Iter.html" title="struct dashmap::iter::Iter">Iter</a></dt><dd>Iterator over a DashMap yielding immutable references.</dd><dt><a class="struct" href="struct.IterMut.html" title="struct dashmap::iter::IterMut">IterMut</a></dt><dd>Iterator over a DashMap yielding mutable references.</dd><dt><a class="struct" href="struct.OwningIter.html" title="struct dashmap::iter::OwningIter">Owning<wbr>Iter</a></dt><dd>Iterator over a DashMap yielding key value pairs.</dd></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/iter/sidebar-items.js
Normal file
1
target/doc/dashmap/iter/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["Iter","IterMut","OwningIter"]};
|
||||
205
target/doc/dashmap/iter/struct.Iter.html
Normal file
205
target/doc/dashmap/iter/struct.Iter.html
Normal file
File diff suppressed because one or more lines are too long
203
target/doc/dashmap/iter/struct.IterMut.html
Normal file
203
target/doc/dashmap/iter/struct.IterMut.html
Normal file
File diff suppressed because one or more lines are too long
227
target/doc/dashmap/iter/struct.OwningIter.html
Normal file
227
target/doc/dashmap/iter/struct.OwningIter.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/iter_set/index.html
Normal file
1
target/doc/dashmap/iter_set/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `iter_set` mod in crate `dashmap`."><title>dashmap::iter_set - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module iter_set</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module iter_<wbr>set</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2 class="in-crate"><a href="../index.html">In crate dashmap</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../index.html">dashmap</a></div><h1>Module <span>iter_<wbr>set</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../src/dashmap/iter_set.rs.html#1-71">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.Iter.html" title="struct dashmap::iter_set::Iter">Iter</a></dt><dt><a class="struct" href="struct.OwningIter.html" title="struct dashmap::iter_set::OwningIter">Owning<wbr>Iter</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/iter_set/sidebar-items.js
Normal file
1
target/doc/dashmap/iter_set/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["Iter","OwningIter"]};
|
||||
194
target/doc/dashmap/iter_set/struct.Iter.html
Normal file
194
target/doc/dashmap/iter_set/struct.Iter.html
Normal file
File diff suppressed because one or more lines are too long
216
target/doc/dashmap/iter_set/struct.OwningIter.html
Normal file
216
target/doc/dashmap/iter_set/struct.OwningIter.html
Normal file
File diff suppressed because one or more lines are too long
38
target/doc/dashmap/mapref/entry/enum.Entry.html
Normal file
38
target/doc/dashmap/mapref/entry/enum.Entry.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/mapref/entry/index.html
Normal file
1
target/doc/dashmap/mapref/entry/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `entry` mod in crate `dashmap`."><title>dashmap::mapref::entry - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../../" data-static-root-path="../../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module entry</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module entry</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li><li><a href="#enums" title="Enums">Enums</a></li></ul></section><div id="rustdoc-modnav"><h2><a href="../index.html">In dashmap::<wbr>mapref</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../../index.html">dashmap</a>::<wbr><a href="../index.html">mapref</a></div><h1>Module <span>entry</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../../src/dashmap/mapref/entry.rs.html#1-283">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.OccupiedEntry.html" title="struct dashmap::mapref::entry::OccupiedEntry">Occupied<wbr>Entry</a></dt><dt><a class="struct" href="struct.VacantEntry.html" title="struct dashmap::mapref::entry::VacantEntry">Vacant<wbr>Entry</a></dt></dl><h2 id="enums" class="section-header">Enums<a href="#enums" class="anchor">§</a></h2><dl class="item-table"><dt><a class="enum" href="enum.Entry.html" title="enum dashmap::mapref::entry::Entry">Entry</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/mapref/entry/sidebar-items.js
Normal file
1
target/doc/dashmap/mapref/entry/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"enum":["Entry"],"struct":["OccupiedEntry","VacantEntry"]};
|
||||
13
target/doc/dashmap/mapref/entry/struct.OccupiedEntry.html
Normal file
13
target/doc/dashmap/mapref/entry/struct.OccupiedEntry.html
Normal file
File diff suppressed because one or more lines are too long
15
target/doc/dashmap/mapref/entry/struct.VacantEntry.html
Normal file
15
target/doc/dashmap/mapref/entry/struct.VacantEntry.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/mapref/index.html
Normal file
1
target/doc/dashmap/mapref/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `mapref` mod in crate `dashmap`."><title>dashmap::mapref - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module mapref</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module mapref</a></h2><h3><a href="#modules">Module Items</a></h3><ul class="block"><li><a href="#modules" title="Modules">Modules</a></li></ul></section><div id="rustdoc-modnav"><h2 class="in-crate"><a href="../index.html">In crate dashmap</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../index.html">dashmap</a></div><h1>Module <span>mapref</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../src/dashmap/mapref/mod.rs.html#1-3">Source</a> </span></div><h2 id="modules" class="section-header">Modules<a href="#modules" class="anchor">§</a></h2><dl class="item-table"><dt><a class="mod" href="entry/index.html" title="mod dashmap::mapref::entry">entry</a></dt><dt><a class="mod" href="multiple/index.html" title="mod dashmap::mapref::multiple">multiple</a></dt><dt><a class="mod" href="one/index.html" title="mod dashmap::mapref::one">one</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/mapref/multiple/index.html
Normal file
1
target/doc/dashmap/mapref/multiple/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `multiple` mod in crate `dashmap`."><title>dashmap::mapref::multiple - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../../" data-static-root-path="../../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module multiple</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module multiple</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2><a href="../index.html">In dashmap::<wbr>mapref</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../../index.html">dashmap</a>::<wbr><a href="../index.html">mapref</a></div><h1>Module <span>multiple</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../../src/dashmap/mapref/multiple.rs.html#1-105">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.RefMulti.html" title="struct dashmap::mapref::multiple::RefMulti">RefMulti</a></dt><dt><a class="struct" href="struct.RefMutMulti.html" title="struct dashmap::mapref::multiple::RefMutMulti">RefMut<wbr>Multi</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/mapref/multiple/sidebar-items.js
Normal file
1
target/doc/dashmap/mapref/multiple/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["RefMulti","RefMutMulti"]};
|
||||
13
target/doc/dashmap/mapref/multiple/struct.RefMulti.html
Normal file
13
target/doc/dashmap/mapref/multiple/struct.RefMulti.html
Normal file
File diff suppressed because one or more lines are too long
13
target/doc/dashmap/mapref/multiple/struct.RefMutMulti.html
Normal file
13
target/doc/dashmap/mapref/multiple/struct.RefMutMulti.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/mapref/one/index.html
Normal file
1
target/doc/dashmap/mapref/one/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `one` mod in crate `dashmap`."><title>dashmap::mapref::one - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../../" data-static-root-path="../../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module one</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module one</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2><a href="../index.html">In dashmap::<wbr>mapref</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../../index.html">dashmap</a>::<wbr><a href="../index.html">mapref</a></div><h1>Module <span>one</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../../src/dashmap/mapref/one.rs.html#1-334">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.MappedRef.html" title="struct dashmap::mapref::one::MappedRef">Mapped<wbr>Ref</a></dt><dt><a class="struct" href="struct.MappedRefMut.html" title="struct dashmap::mapref::one::MappedRefMut">Mapped<wbr>RefMut</a></dt><dt><a class="struct" href="struct.Ref.html" title="struct dashmap::mapref::one::Ref">Ref</a></dt><dt><a class="struct" href="struct.RefMut.html" title="struct dashmap::mapref::one::RefMut">RefMut</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/mapref/one/sidebar-items.js
Normal file
1
target/doc/dashmap/mapref/one/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["MappedRef","MappedRefMut","Ref","RefMut"]};
|
||||
16
target/doc/dashmap/mapref/one/struct.MappedRef.html
Normal file
16
target/doc/dashmap/mapref/one/struct.MappedRef.html
Normal file
File diff suppressed because one or more lines are too long
15
target/doc/dashmap/mapref/one/struct.MappedRefMut.html
Normal file
15
target/doc/dashmap/mapref/one/struct.MappedRefMut.html
Normal file
File diff suppressed because one or more lines are too long
15
target/doc/dashmap/mapref/one/struct.Ref.html
Normal file
15
target/doc/dashmap/mapref/one/struct.Ref.html
Normal file
File diff suppressed because one or more lines are too long
15
target/doc/dashmap/mapref/one/struct.RefMut.html
Normal file
15
target/doc/dashmap/mapref/one/struct.RefMut.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/mapref/sidebar-items.js
Normal file
1
target/doc/dashmap/mapref/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"mod":["entry","multiple","one"]};
|
||||
11
target/doc/dashmap/read_only/struct.ReadOnlyView.html
Normal file
11
target/doc/dashmap/read_only/struct.ReadOnlyView.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;URL=../../dashmap/struct.ReadOnlyView.html">
|
||||
<title>Redirection</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="../../dashmap/struct.ReadOnlyView.html">../../dashmap/struct.ReadOnlyView.html</a>...</p>
|
||||
<script>location.replace("../../dashmap/struct.ReadOnlyView.html" + location.search + location.hash);</script>
|
||||
</body>
|
||||
</html>
|
||||
11
target/doc/dashmap/set/struct.DashSet.html
Normal file
11
target/doc/dashmap/set/struct.DashSet.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;URL=../../dashmap/struct.DashSet.html">
|
||||
<title>Redirection</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="../../dashmap/struct.DashSet.html">../../dashmap/struct.DashSet.html</a>...</p>
|
||||
<script>location.replace("../../dashmap/struct.DashSet.html" + location.search + location.hash);</script>
|
||||
</body>
|
||||
</html>
|
||||
1
target/doc/dashmap/setref/index.html
Normal file
1
target/doc/dashmap/setref/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `setref` mod in crate `dashmap`."><title>dashmap::setref - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module setref</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module setref</a></h2><h3><a href="#modules">Module Items</a></h3><ul class="block"><li><a href="#modules" title="Modules">Modules</a></li></ul></section><div id="rustdoc-modnav"><h2 class="in-crate"><a href="../index.html">In crate dashmap</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../index.html">dashmap</a></div><h1>Module <span>setref</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../src/dashmap/setref/mod.rs.html#1-2">Source</a> </span></div><h2 id="modules" class="section-header">Modules<a href="#modules" class="anchor">§</a></h2><dl class="item-table"><dt><a class="mod" href="multiple/index.html" title="mod dashmap::setref::multiple">multiple</a></dt><dt><a class="mod" href="one/index.html" title="mod dashmap::setref::one">one</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/setref/multiple/index.html
Normal file
1
target/doc/dashmap/setref/multiple/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `multiple` mod in crate `dashmap`."><title>dashmap::setref::multiple - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../../" data-static-root-path="../../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module multiple</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module multiple</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2><a href="../index.html">In dashmap::<wbr>setref</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../../index.html">dashmap</a>::<wbr><a href="../index.html">setref</a></div><h1>Module <span>multiple</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../../src/dashmap/setref/multiple.rs.html#1-25">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.RefMulti.html" title="struct dashmap::setref::multiple::RefMulti">RefMulti</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/setref/multiple/sidebar-items.js
Normal file
1
target/doc/dashmap/setref/multiple/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["RefMulti"]};
|
||||
15
target/doc/dashmap/setref/multiple/struct.RefMulti.html
Normal file
15
target/doc/dashmap/setref/multiple/struct.RefMulti.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/setref/one/index.html
Normal file
1
target/doc/dashmap/setref/one/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `one` mod in crate `dashmap`."><title>dashmap::setref::one - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../../" data-static-root-path="../../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module one</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module one</a></h2><h3><a href="#structs">Module Items</a></h3><ul class="block"><li><a href="#structs" title="Structs">Structs</a></li></ul></section><div id="rustdoc-modnav"><h2><a href="../index.html">In dashmap::<wbr>setref</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../../index.html">dashmap</a>::<wbr><a href="../index.html">setref</a></div><h1>Module <span>one</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../../src/dashmap/setref/one.rs.html#1-25">Source</a> </span></div><h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2><dl class="item-table"><dt><a class="struct" href="struct.Ref.html" title="struct dashmap::setref::one::Ref">Ref</a></dt></dl></section></div></main></body></html>
|
||||
1
target/doc/dashmap/setref/one/sidebar-items.js
Normal file
1
target/doc/dashmap/setref/one/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"struct":["Ref"]};
|
||||
15
target/doc/dashmap/setref/one/struct.Ref.html
Normal file
15
target/doc/dashmap/setref/one/struct.Ref.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/setref/sidebar-items.js
Normal file
1
target/doc/dashmap/setref/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"mod":["multiple","one"]};
|
||||
1
target/doc/dashmap/sidebar-items.js
Normal file
1
target/doc/dashmap/sidebar-items.js
Normal file
@@ -0,0 +1 @@
|
||||
window.SIDEBAR_ITEMS = {"mod":["iter","iter_set","mapref","setref","try_result"],"struct":["DashMap","DashSet","ReadOnlyView","TryReserveError"],"trait":["Map"]};
|
||||
378
target/doc/dashmap/struct.DashMap.html
Normal file
378
target/doc/dashmap/struct.DashMap.html
Normal file
File diff suppressed because one or more lines are too long
137
target/doc/dashmap/struct.DashSet.html
Normal file
137
target/doc/dashmap/struct.DashSet.html
Normal file
File diff suppressed because one or more lines are too long
41
target/doc/dashmap/struct.ReadOnlyView.html
Normal file
41
target/doc/dashmap/struct.ReadOnlyView.html
Normal file
File diff suppressed because one or more lines are too long
16
target/doc/dashmap/struct.TryReserveError.html
Normal file
16
target/doc/dashmap/struct.TryReserveError.html
Normal file
File diff suppressed because one or more lines are too long
11
target/doc/dashmap/t/trait.Map.html
Normal file
11
target/doc/dashmap/t/trait.Map.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;URL=../../dashmap/trait.Map.html">
|
||||
<title>Redirection</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="../../dashmap/trait.Map.html">../../dashmap/trait.Map.html</a>...</p>
|
||||
<script>location.replace("../../dashmap/trait.Map.html" + location.search + location.hash);</script>
|
||||
</body>
|
||||
</html>
|
||||
135
target/doc/dashmap/trait.Map.html
Normal file
135
target/doc/dashmap/trait.Map.html
Normal file
File diff suppressed because one or more lines are too long
32
target/doc/dashmap/try_result/enum.TryResult.html
Normal file
32
target/doc/dashmap/try_result/enum.TryResult.html
Normal file
File diff suppressed because one or more lines are too long
1
target/doc/dashmap/try_result/index.html
Normal file
1
target/doc/dashmap/try_result/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="API documentation for the Rust `try_result` mod in crate `dashmap`."><title>dashmap::try_result - Rust</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-ca0dd0c4.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="dashmap" data-themes="" data-resource-suffix="" data-rustdoc-version="1.93.1 (01f6ddf75 2026-02-11)" data-channel="1.93.1" data-search-js="search-9e2438ea.js" data-stringdex-js="stringdex-a3946164.js" data-settings-js="settings-c38705f0.js" ><script src="../../static.files/storage-e2aeef58.js"></script><script defer src="../sidebar-items.js"></script><script defer src="../../static.files/main-a410ff4d.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-263c88ec.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc mod"><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><rustdoc-topbar><h2><a href="#">Module try_result</a></h2></rustdoc-topbar><nav class="sidebar"><div class="sidebar-crate"><h2><a href="../../dashmap/index.html">dashmap</a><span class="version">6.1.0</span></h2></div><div class="sidebar-elems"><section id="rustdoc-toc"><h2 class="location"><a href="#">Module try_<wbr>result</a></h2><h3><a href="#enums">Module Items</a></h3><ul class="block"><li><a href="#enums" title="Enums">Enums</a></li></ul></section><div id="rustdoc-modnav"><h2 class="in-crate"><a href="../index.html">In crate dashmap</a></h2></div></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><div class="width-limiter"><section id="main-content" class="content"><div class="main-heading"><div class="rustdoc-breadcrumbs"><a href="../index.html">dashmap</a></div><h1>Module <span>try_<wbr>result</span> <button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1><rustdoc-toolbar></rustdoc-toolbar><span class="sub-heading"><a class="src" href="../../src/dashmap/try_result.rs.html#1-46">Source</a> </span></div><h2 id="enums" class="section-header">Enums<a href="#enums" class="anchor">§</a></h2><dl class="item-table"><dt><a class="enum" href="enum.TryResult.html" title="enum dashmap::try_result::TryResult">TryResult</a></dt><dd>Represents the result of a non-blocking read from a <a href="../struct.DashMap.html" title="struct dashmap::DashMap">DashMap</a>.</dd></dl></section></div></main></body></html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user