diff --git a/CLAUDE.md b/CLAUDE.md index ae49941..bce2b73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ ZCLAW 是面向中文用户的 AI Agent 桌面端,核心能力包括: - **智能对话** - 多模型支持、流式响应、上下文管理 -- **自主能力** - 8 个 Hands(浏览器、数据采集、研究、预测等) +- **自主能力** - 11 个 Hands(9 启用 + 2 禁用: Predictor, Lead) - **技能系统** - 可扩展的 SKILL.md 技能定义 - **工作流编排** - 多步骤自动化任务 - **安全审计** - 完整的操作日志和权限控制 @@ -69,7 +69,7 @@ ZCLAW/ | 桌面框架 | Tauri 2.x | | 样式方案 | Tailwind CSS | | 配置格式 | TOML | -| 后端核心 | Rust Workspace (9 crates) | +| 后端核心 | Rust Workspace (10 crates) | | SaaS 后端 | Axum + PostgreSQL (zclaw-saas) | | 管理后台 | Next.js (admin/) | diff --git a/Cargo.lock b/Cargo.lock index 165a3a2..787709f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,7 +1314,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2", - "sqlx", + "sqlx 0.7.4", "tauri", "tauri-build", "tauri-plugin-opener", @@ -2262,6 +2262,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -2285,6 +2287,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "headers" version = "0.4.1" @@ -3716,6 +3727,15 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "sqlx 0.8.6", +] + [[package]] name = "phf" version = "0.8.0" @@ -4571,6 +4591,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2", "signature", "spki", "subtle", @@ -5271,13 +5292,24 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ - "sqlx-core", - "sqlx-macros", + "sqlx-core 0.7.4", + "sqlx-macros 0.7.4", "sqlx-mysql", - "sqlx-postgres", + "sqlx-postgres 0.7.4", "sqlx-sqlite", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core 0.8.6", + "sqlx-macros 0.8.6", + "sqlx-postgres 0.8.6", +] + [[package]] name = "sqlx-core" version = "0.7.4" @@ -5288,6 +5320,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -5297,7 +5330,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashlink", + "hashlink 0.8.4", "hex", "indexmap 2.13.0", "log", @@ -5317,6 +5350,38 @@ dependencies = [ "url", ] +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink 0.10.0", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tracing", + "url", +] + [[package]] name = "sqlx-macros" version = "0.7.4" @@ -5325,11 +5390,24 @@ checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", - "sqlx-core", - "sqlx-macros-core", + "sqlx-core 0.7.4", + "sqlx-macros-core 0.7.4", "syn 1.0.109", ] +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core 0.8.6", + "sqlx-macros-core 0.8.6", + "syn 2.0.117", +] + [[package]] name = "sqlx-macros-core" version = "0.7.4" @@ -5346,9 +5424,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "sqlx-core", + "sqlx-core 0.7.4", "sqlx-mysql", - "sqlx-postgres", + "sqlx-postgres 0.7.4", "sqlx-sqlite", "syn 1.0.109", "tempfile", @@ -5356,6 +5434,28 @@ dependencies = [ "url", ] +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core 0.8.6", + "sqlx-postgres 0.8.6", + "syn 2.0.117", + "url", +] + [[package]] name = "sqlx-mysql" version = "0.7.4" @@ -5367,6 +5467,7 @@ dependencies = [ "bitflags 2.11.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -5391,7 +5492,7 @@ dependencies = [ "sha1", "sha2", "smallvec", - "sqlx-core", + "sqlx-core 0.7.4", "stringprep", "thiserror 1.0.69", "tracing", @@ -5408,6 +5509,7 @@ dependencies = [ "base64 0.21.7", "bitflags 2.11.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -5429,13 +5531,50 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "sqlx-core", + "sqlx-core 0.7.4", "stringprep", "thiserror 1.0.69", "tracing", "whoami", ] +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core 0.8.6", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + [[package]] name = "sqlx-sqlite" version = "0.7.4" @@ -5443,6 +5582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -5453,7 +5593,7 @@ dependencies = [ "log", "percent-encoding", "serde", - "sqlx-core", + "sqlx-core 0.7.4", "tracing", "url", "urlencoding", @@ -8211,7 +8351,7 @@ dependencies = [ "libsqlite3-sys", "serde", "serde_json", - "sqlx", + "sqlx 0.7.4", "thiserror 2.0.18", "tokio", "tokio-test", @@ -8227,11 +8367,9 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", - "hmac", "reqwest 0.12.28", "serde", "serde_json", - "sha1", "thiserror 2.0.18", "tokio", "tracing", @@ -8272,12 +8410,14 @@ dependencies = [ name = "zclaw-memory" version = "0.1.0" dependencies = [ + "anyhow", + "async-trait", "chrono", "futures", "libsqlite3-sys", "serde", "serde_json", - "sqlx", + "sqlx 0.7.4", "thiserror 2.0.18", "tokio", "tracing", @@ -8362,9 +8502,11 @@ dependencies = [ "aes-gcm", "anyhow", "argon2", + "async-stream", "async-trait", "axum", "axum-extra", + "base64 0.22.1", "bytes", "chrono", "dashmap", @@ -8372,15 +8514,17 @@ dependencies = [ "futures", "hex", "jsonwebtoken", + "pgvector", "rand 0.8.5", "regex", "reqwest 0.12.28", + "rsa", "secrecy", "serde", "serde_json", "sha2", "socket2 0.5.10", - "sqlx", + "sqlx 0.7.4", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index f439cbe..b844d90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ rand = "0.8" # Crypto sha2 = "0.10" aes-gcm = "0.10" +rsa = { version = "0.9", features = ["pem"] } # Home directory dirs = "6" diff --git a/admin-v2/src/layouts/AdminLayout.tsx b/admin-v2/src/layouts/AdminLayout.tsx index 1887c7c..ec67140 100644 --- a/admin-v2/src/layouts/AdminLayout.tsx +++ b/admin-v2/src/layouts/AdminLayout.tsx @@ -18,6 +18,7 @@ import { ApiOutlined, BookOutlined, CrownOutlined, + SafetyOutlined, } from '@ant-design/icons' import { Avatar, Dropdown, Tooltip, Drawer } from 'antd' import { useAuthStore } from '@/stores/authStore' @@ -39,6 +40,7 @@ interface NavItem { const navItems: NavItem[] = [ { path: '/', name: '仪表盘', icon: , group: '核心' }, { path: '/accounts', name: '账号管理', icon: , permission: 'account:admin', group: '资源管理' }, + { path: '/roles', name: '角色与权限', icon: , permission: 'account:admin', group: '资源管理' }, { path: '/model-services', name: '模型服务', icon: , permission: 'provider:manage', group: '资源管理' }, { path: '/agent-templates', name: 'Agent 模板', icon: , permission: 'model:read', group: '资源管理' }, { path: '/api-keys', name: 'API 密钥', icon: , permission: 'provider:manage', group: '资源管理' }, @@ -201,6 +203,7 @@ function MobileDrawer({ const breadcrumbMap: Record = { '/': '仪表盘', '/accounts': '账号管理', + '/roles': '角色与权限', '/model-services': '模型服务', '/providers': '模型服务', '/models': '模型服务', diff --git a/admin-v2/src/pages/Roles.tsx b/admin-v2/src/pages/Roles.tsx new file mode 100644 index 0000000..cbbd847 --- /dev/null +++ b/admin-v2/src/pages/Roles.tsx @@ -0,0 +1,509 @@ +// ============================================================ +// 角色与权限模板管理 +// ============================================================ + +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Button, + message, + Tag, + Modal, + Form, + Input, + Select, + Space, + Popconfirm, + Tabs, + Tooltip, +} from 'antd' +import { PlusOutlined, SafetyOutlined, CheckCircleOutlined } from '@ant-design/icons' +import type { ProColumns } from '@ant-design/pro-components' +import { ProTable } from '@ant-design/pro-components' +import { roleService } from '@/services/roles' +import { PageHeader } from '@/components/PageHeader' +import type { + Role, + PermissionTemplate, + CreateRoleRequest, + UpdateRoleRequest, + CreateTemplateRequest, +} from '@/types' + +// ============================================================ +// 常见权限选项 +// ============================================================ +const permissionOptions = [ + { value: 'account:admin', label: 'account:admin' }, + { value: 'provider:manage', label: 'provider:manage' }, + { value: 'model:read', label: 'model:read' }, + { value: 'model:write', label: 'model:write' }, + { value: 'relay:use', label: 'relay:use' }, + { value: 'knowledge:read', label: 'knowledge:read' }, + { value: 'knowledge:write', label: 'knowledge:write' }, + { value: 'billing:read', label: 'billing:read' }, + { value: 'billing:write', label: 'billing:write' }, + { value: 'config:read', label: 'config:read' }, + { value: 'config:write', label: 'config:write' }, + { value: 'prompt:read', label: 'prompt:read' }, + { value: 'prompt:write', label: 'prompt:write' }, + { value: 'admin:full', label: 'admin:full' }, +] + +// ============================================================ +// Roles Tab +// ============================================================ +function RolesTab() { + const queryClient = useQueryClient() + const [form] = Form.useForm() + const [modalOpen, setModalOpen] = useState(false) + const [editingId, setEditingId] = useState(null) + + const { data, isLoading } = useQuery({ + queryKey: ['roles'], + queryFn: ({ signal }) => roleService.list(signal), + }) + + const createMutation = useMutation({ + mutationFn: (data: CreateRoleRequest) => roleService.create(data), + onSuccess: () => { + message.success('角色已创建') + queryClient.invalidateQueries({ queryKey: ['roles'] }) + setModalOpen(false) + form.resetFields() + }, + onError: (err: Error) => message.error(err.message || '创建失败'), + }) + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateRoleRequest }) => + roleService.update(id, data), + onSuccess: () => { + message.success('角色已更新') + queryClient.invalidateQueries({ queryKey: ['roles'] }) + setModalOpen(false) + }, + onError: (err: Error) => message.error(err.message || '更新失败'), + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => roleService.delete(id), + onSuccess: () => { + message.success('角色已删除') + queryClient.invalidateQueries({ queryKey: ['roles'] }) + }, + onError: (err: Error) => message.error(err.message || '删除失败'), + }) + + const handleSave = async () => { + const values = await form.validateFields() + if (editingId) { + updateMutation.mutate({ id: editingId, data: values }) + } else { + createMutation.mutate(values) + } + } + + const openEdit = async (record: Role) => { + setEditingId(record.id) + const permissions = await roleService.getPermissions(record.id).catch(() => record.permissions) + form.setFieldsValue({ ...record, permissions }) + setModalOpen(true) + } + + const openCreate = () => { + setEditingId(null) + form.resetFields() + setModalOpen(true) + } + + const closeModal = () => { + setModalOpen(false) + setEditingId(null) + form.resetFields() + } + + const columns: ProColumns[] = [ + { + title: '角色名称', + dataIndex: 'name', + width: 160, + render: (_, record) => ( + + {record.name} + + ), + }, + { + title: '描述', + dataIndex: 'description', + width: 240, + ellipsis: true, + render: (_, record) => record.description || '-', + }, + { + title: '权限数', + dataIndex: 'permissions', + width: 100, + render: (_, record) => ( + + {record.permissions?.length ?? 0} 项 + + ), + }, + { + title: '关联账号', + dataIndex: 'account_count', + width: 100, + render: (_, record) => record.account_count ?? 0, + }, + { + title: '创建时间', + dataIndex: 'created_at', + width: 180, + render: (_, record) => + record.created_at ? new Date(record.created_at).toLocaleString('zh-CN') : '-', + }, + { + title: '操作', + width: 160, + render: (_, record) => ( + + + deleteMutation.mutate(record.id)} + > + + + + ), + }, + ] + + return ( +
+ + columns={columns} + dataSource={data ?? []} + loading={isLoading} + rowKey="id" + search={false} + toolBarRender={() => [ + , + ]} + pagination={{ showSizeChanger: false }} + /> + + +
+ + + + + + + + + + + + + +