first commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
# MySQL 配置
|
||||||
|
MYSQL_ROOT_PASSWORD=root123
|
||||||
|
MYSQL_DATABASE=sentclaw
|
||||||
|
MYSQL_USER=sentclaw
|
||||||
|
MYSQL_PASSWORD=sentclaw123
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_PASSWORD=redis123
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# 后端配置
|
||||||
|
FLASK_ENV=development
|
||||||
|
FLASK_DEBUG=True
|
||||||
|
FLASK_HOST=0.0.0.0
|
||||||
|
FLASK_PORT=5000
|
||||||
|
SECRET_KEY=your-secret-key-change-this-in-production
|
||||||
|
|
||||||
|
# 数据库连接(Flask)
|
||||||
|
DATABASE_URL=mysql+pymysql://sentclaw:sentclaw123@localhost:3306/sentclaw
|
||||||
|
|
||||||
|
# Redis 连接
|
||||||
|
REDIS_URL=redis://:redis123@localhost:6379/0
|
||||||
|
|
||||||
|
# LLM 配置
|
||||||
|
DASHSCOPE_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET_KEY=your-jwt-secret-key-change-this
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRES=3600
|
||||||
|
|
||||||
|
# 文件上传
|
||||||
|
UPLOAD_FOLDER=uploads
|
||||||
|
MAX_CONTENT_LENGTH=16777216
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
LOG_FILE=logs/app.log
|
||||||
|
|
||||||
|
# CORS 配置
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
# 环境变量
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# 虚拟环境
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# 数据
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
*.sqlite
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
|
||||||
|
# Alembic
|
||||||
|
alembic/versions/*.pyc
|
||||||
|
|
||||||
|
# Node.js / 前端
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Electron
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
*.dmg
|
||||||
|
*.exe
|
||||||
|
*.AppImage
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.docker/
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# 敏感文件
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
secrets/
|
||||||
|
credentials.json
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# 项目结构
|
||||||
|
|
||||||
|
**技术栈:**
|
||||||
|
- 后端: Python + MySQL + Redis(参考 mateclaw-server 的 Spring Boot 架构)
|
||||||
|
- 前端: Electron(参考 mateclaw-desktop)
|
||||||
|
- Web 页面: Vite + Vue3(参考 mateclaw-ui)
|
||||||
|
- 目标: openclaw/mateclaw 的克隆项目
|
||||||
|
|
||||||
|
**参考项目架构(基于 mateclaw):**
|
||||||
|
```
|
||||||
|
SentClaw/
|
||||||
|
├── backend/ # Python 后端(替换 Spring Boot)
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API 路由
|
||||||
|
│ │ ├── models/ # SQLAlchemy 数据库模型
|
||||||
|
│ │ ├── schemas/ # Marshmallow 序列化/验证
|
||||||
|
│ │ ├── services/ # 业务逻辑层
|
||||||
|
│ │ ├── agents/ # Agent 引擎
|
||||||
|
│ │ ├── tools/ # 工具系统 + MCP 适配器
|
||||||
|
│ │ ├── skills/ # 技能管理
|
||||||
|
│ │ ├── channels/ # 渠道适配器
|
||||||
|
│ │ ├── workspace/# 工作空间和文件管理
|
||||||
|
│ │ ├── memory/ # 记忆系统
|
||||||
|
│ │ ├── llm/ # LLM 多厂商接入
|
||||||
|
│ │ └── core/ # 核心配置(config.py)
|
||||||
|
│ ├── alembic/ # 数据库迁移
|
||||||
|
│ ├── tests/ # 测试
|
||||||
|
│ ├── run.py # 应用入口
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── web/ # Vite + Vue3 Web 前端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API 调用封装
|
||||||
|
│ │ ├── views/ # 页面组件
|
||||||
|
│ │ ├── stores/ # Pinia 状态管理
|
||||||
|
│ │ ├── router/ # Vue Router 配置
|
||||||
|
│ │ └── types/ # TypeScript 类型定义
|
||||||
|
│ └── package.json
|
||||||
|
├── desktop/ # Electron 桌面应用
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main/ # Electron 主进程
|
||||||
|
│ ├── preload/ # 预加载脚本
|
||||||
|
│ └── renderer/ # 渲染进程(Vue3)
|
||||||
|
└── scripts/ # 脚本
|
||||||
|
└── setup.sh # 一键初始化脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心模块:**
|
||||||
|
- Agent 引擎:ReAct、Plan-and-Execute、StateGraph
|
||||||
|
- 工具系统:内置工具 + MCP 协议
|
||||||
|
- 技能管理:技能包 + ClawHub 市场
|
||||||
|
- 记忆系统:短期记忆 + 对话后提取 + 记忆整合
|
||||||
|
- 多渠道接入:Web、钉钉、飞书、Telegram、Discord 等
|
||||||
|
- 多厂商模型:20+ LLM 厂商支持
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
|
||||||
|
### 快速初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 一键初始化所有环境
|
||||||
|
./scripts/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 MySQL + Redis
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 创建虚拟环境(首次)
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 数据库迁移
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
python run.py # 运行在 http://localhost:5000
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
black app/
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
flake8 app/
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
mypy app/
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
|
||||||
|
# 安装依赖(首次)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev # 运行在 http://localhost:5173
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 桌面应用开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd desktop
|
||||||
|
|
||||||
|
# 安装依赖(首次)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发模式
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 打包应用
|
||||||
|
npm run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发服务器启动顺序
|
||||||
|
|
||||||
|
1. **启动 Docker 服务**(MySQL + Redis)
|
||||||
|
2. **初始化后端**(虚拟环境、依赖、数据库迁移)
|
||||||
|
3. **启动后端服务**(python run.py)
|
||||||
|
4. **启动前端服务**(npm run dev,新终端)
|
||||||
|
5. **启动桌面应用**(可选,npm run dev,新终端)
|
||||||
|
|
||||||
|
## 包边界和入口点
|
||||||
|
|
||||||
|
- **后端入口**: `backend/run.py` → Flask 应用工厂 `app/__init__.py:create_app()`
|
||||||
|
- **前端入口**: `web/src/main.ts` → Vue 根组件 `App.vue`
|
||||||
|
- **桌面应用入口**: `desktop/src/main/index.ts` → Electron 主进程
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
- **环境变量**: `.env`(从 `.env.example` 复制)
|
||||||
|
- **后端配置**: `backend/app/config.py`
|
||||||
|
- **前端配置**: `web/vite.config.ts`
|
||||||
|
- **桌面应用配置**: `desktop/electron.vite.config.ts`
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
- **数据库**: MySQL 8.0
|
||||||
|
- **ORM**: SQLAlchemy
|
||||||
|
- **迁移工具**: Alembic
|
||||||
|
- **初始化脚本**: `scripts/init-db.sql`
|
||||||
|
- **默认账号**: admin / admin123
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保 Docker 服务在启动后端之前已运行
|
||||||
|
2. 前端通过 Vite 代理访问后端 API(/api → http://localhost:5000)
|
||||||
|
3. 修改后端模型后需要运行 `flask db migrate` 和 `flask db upgrade`
|
||||||
|
4. 前端和后端都需要各自安装依赖
|
||||||
|
5. 首次运行需要初始化数据库(`flask db upgrade`)
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
# SentClaw
|
||||||
|
|
||||||
|
> 🤖 基于 Python + Vue3 的 AI 智能助手系统,支持多 Agent 编排、Skills、Memory、MCP 协议与多渠道接入
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
SentClaw 是 openclaw/mateclaw 的克隆项目,旨在提供一个功能完整、易于扩展的个人 AI 助手系统。
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
- **多 Agent 编排** — ReAct、Plan-and-Execute、StateGraph
|
||||||
|
- **工具与技能系统** — 内置工具 + MCP 协议 + ClawHub 市场
|
||||||
|
- **多层记忆** — 短期记忆 + 对话后提取 + 记忆整合
|
||||||
|
- **多渠道接入** — Web、钉钉、飞书、Telegram、Discord
|
||||||
|
- **多厂商模型** — 支持 20+ LLM 厂商
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **框架**: Flask 3.0
|
||||||
|
- **数据库**: MySQL 8.0 + Redis 7
|
||||||
|
- **ORM**: SQLAlchemy 2.0
|
||||||
|
- **迁移**: Alembic
|
||||||
|
- **认证**: Flask-JWT-Extended
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **框架**: Vue 3 + TypeScript
|
||||||
|
- **构建工具**: Vite 5
|
||||||
|
- **UI 组件**: Element Plus
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
- **路由**: Vue Router
|
||||||
|
|
||||||
|
### 桌面应用
|
||||||
|
- **框架**: Electron + electron-vite
|
||||||
|
- **渲染进程**: Vue 3(复用 Web 前端)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Node.js 18+
|
||||||
|
- Python 3.9+
|
||||||
|
|
||||||
|
### 一键初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone <repository-url>
|
||||||
|
cd SentClaw
|
||||||
|
|
||||||
|
# 运行初始化脚本
|
||||||
|
./scripts/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动安装
|
||||||
|
|
||||||
|
#### 1. 启动 Docker 服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制环境变量
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 启动 MySQL 和 Redis
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 后端设置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 创建虚拟环境
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 初始化数据库
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 前端设置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 桌面应用(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd desktop
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发模式
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问应用
|
||||||
|
|
||||||
|
- **Web 前端**: http://localhost:5173
|
||||||
|
- **后端 API**: http://localhost:5000
|
||||||
|
- **默认账号**: admin / admin123
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
SentClaw/
|
||||||
|
├── backend/ # Python 后端
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API 路由
|
||||||
|
│ │ ├── models/ # 数据库模型
|
||||||
|
│ │ ├── schemas/ # 序列化/验证
|
||||||
|
│ │ ├── services/ # 业务逻辑
|
||||||
|
│ │ ├── agents/ # Agent 引擎
|
||||||
|
│ │ ├── tools/ # 工具系统
|
||||||
|
│ │ ├── skills/ # 技能管理
|
||||||
|
│ │ ├── channels/ # 渠道适配器
|
||||||
|
│ │ ├── workspace/# 工作空间
|
||||||
|
│ │ ├── memory/ # 记忆系统
|
||||||
|
│ │ ├── llm/ # LLM 接入
|
||||||
|
│ │ └── core/ # 核心配置
|
||||||
|
│ ├── alembic/ # 数据库迁移
|
||||||
|
│ ├── tests/ # 测试
|
||||||
|
│ └── run.py # 应用入口
|
||||||
|
├── web/ # Vue3 Web 前端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API 调用
|
||||||
|
│ │ ├── views/ # 页面
|
||||||
|
│ │ ├── stores/ # Pinia stores
|
||||||
|
│ │ └── router/ # 路由配置
|
||||||
|
│ └── package.json
|
||||||
|
├── desktop/ # Electron 桌面应用
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main/ # 主进程
|
||||||
|
│ ├── preload/ # 预加载脚本
|
||||||
|
│ └── renderer/ # 渲染进程
|
||||||
|
├── scripts/ # 脚本
|
||||||
|
│ └── setup.sh # 初始化脚本
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env.example
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 后端开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
black app/
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
flake8 app/
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
mypy app/
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 桌面应用开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd desktop
|
||||||
|
|
||||||
|
# 开发模式
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 打包应用
|
||||||
|
npm run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 创建新迁移
|
||||||
|
flask db migrate -m "描述信息"
|
||||||
|
|
||||||
|
# 应用迁移
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# 回滚迁移
|
||||||
|
flask db downgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 进入 MySQL 容器
|
||||||
|
docker-compose exec mysql mysql -u sentclaw -p
|
||||||
|
|
||||||
|
# 进入 Redis 容器
|
||||||
|
docker-compose exec redis redis-cli -a redis123
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
主要配置文件:`.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MySQL 配置
|
||||||
|
MYSQL_ROOT_PASSWORD=root123
|
||||||
|
MYSQL_DATABASE=sentclaw
|
||||||
|
MYSQL_USER=sentclaw
|
||||||
|
MYSQL_PASSWORD=sentclaw123
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_PASSWORD=redis123
|
||||||
|
|
||||||
|
# 后端配置
|
||||||
|
FLASK_ENV=development
|
||||||
|
FLASK_DEBUG=True
|
||||||
|
FLASK_PORT=5000
|
||||||
|
SECRET_KEY=your-secret-key
|
||||||
|
|
||||||
|
# 数据库连接
|
||||||
|
DATABASE_URL=mysql+pymysql://sentclaw:sentclaw123@localhost:3306/sentclaw
|
||||||
|
|
||||||
|
# Redis 连接
|
||||||
|
REDIS_URL=redis://:redis123@localhost:6379/0
|
||||||
|
|
||||||
|
# LLM API Keys(根据需要填写)
|
||||||
|
DASHSCOPE_API_KEY=
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 后端无法连接 MySQL
|
||||||
|
|
||||||
|
确保 Docker 服务已启动:
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
检查 .env 中的数据库配置是否正确。
|
||||||
|
|
||||||
|
### 前端无法连接后端 API
|
||||||
|
|
||||||
|
检查后端是否已启动(http://localhost:5000),并查看 Vite 配置中的代理设置。
|
||||||
|
|
||||||
|
### 数据库迁移失败
|
||||||
|
|
||||||
|
删除现有数据库并重新初始化:
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d
|
||||||
|
flask db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
Apache License 2.0
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
本项目参考了 [mateclaw](https://gitee.com/matevip_admin/mateclaw) 的设计和实现。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 100
|
||||||
|
extend-ignore = E203, W503
|
||||||
|
exclude =
|
||||||
|
.git,
|
||||||
|
__pycache__,
|
||||||
|
.venv,
|
||||||
|
venv,
|
||||||
|
build,
|
||||||
|
dist,
|
||||||
|
*.egg-info
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "run.py"]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
sqlalchemy.url = mysql+pymysql://sentclaw:sentclaw123@localhost:3306/sentclaw
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
from app.models import db
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
target_metadata = db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
app = create_app()
|
||||||
|
return app.extensions["sqlalchemy"].engine
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
configuration = config.get_section(config.config_ini_section, {})
|
||||||
|
configuration["sqlalchemy.url"] = os.environ.get(
|
||||||
|
"DATABASE_URL", configuration["sqlalchemy.url"]
|
||||||
|
)
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
configuration,
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_cors import CORS
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .config import config_by_name
|
||||||
|
from .models import db
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_name=None):
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
if config_name is None:
|
||||||
|
config_name = os.environ.get("FLASK_ENV", "development")
|
||||||
|
|
||||||
|
app.config.from_object(config_by_name[config_name])
|
||||||
|
|
||||||
|
CORS(app, origins=app.config["CORS_ORIGINS"])
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
|
jwt = JWTManager(app)
|
||||||
|
|
||||||
|
setup_logging(app)
|
||||||
|
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
setup_error_handlers(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(app):
|
||||||
|
log_dir = os.path.dirname(app.config["LOG_FILE"])
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, app.config["LOG_LEVEL"]),
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
handlers=[logging.FileHandler(app.config["LOG_FILE"]), logging.StreamHandler()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
from .api import api_bp
|
||||||
|
|
||||||
|
app.register_blueprint(api_bp, url_prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_error_handlers(app):
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return {"error": "Not found"}, 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
app.logger.error(f"Internal error: {error}")
|
||||||
|
return {"error": "Internal server error"}, 500
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .engine import AgentEngine
|
||||||
|
from .react import ReActAgent
|
||||||
|
from .plan_execute import PlanAndExecuteAgent
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AgentEngine", "ReActAgent", "PlanAndExecuteAgent"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class AgentEngine:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def run(self, prompt, tools=None):
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .engine import AgentEngine
|
||||||
|
|
||||||
|
|
||||||
|
class PlanAndExecuteAgent(AgentEngine):
|
||||||
|
def run(self, prompt, tools=None):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .engine import AgentEngine
|
||||||
|
|
||||||
|
|
||||||
|
class ReActAgent(AgentEngine):
|
||||||
|
def run(self, prompt, tools=None):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
api_bp = Blueprint("api", __name__)
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
auth,
|
||||||
|
users,
|
||||||
|
workspaces,
|
||||||
|
agents,
|
||||||
|
conversations,
|
||||||
|
messages,
|
||||||
|
tools,
|
||||||
|
skills,
|
||||||
|
memories,
|
||||||
|
models,
|
||||||
|
cron_jobs,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Agent, Workspace
|
||||||
|
from ..schemas import AgentSchema, AgentCreateSchema, AgentUpdateSchema
|
||||||
|
from ..services import AgentService
|
||||||
|
|
||||||
|
|
||||||
|
agent_schema = AgentSchema()
|
||||||
|
agent_create_schema = AgentCreateSchema()
|
||||||
|
agent_update_schema = AgentUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/agents", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_agents(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
agents = Agent.query.filter_by(workspace_id=workspace_id).all()
|
||||||
|
return jsonify(agent_schema.dump(agents, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/agents", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_agent(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = agent_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = AgentService.create_agent(workspace_id, data)
|
||||||
|
return jsonify(agent_schema.dump(agent)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_agent(agent_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
agent = Agent.query.filter_by(id=agent_id).first()
|
||||||
|
if not agent:
|
||||||
|
return jsonify({"error": "Agent 不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=agent.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此 Agent"}), 403
|
||||||
|
|
||||||
|
return jsonify(agent_schema.dump(agent))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_agent(agent_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
agent = Agent.query.filter_by(id=agent_id).first()
|
||||||
|
if not agent:
|
||||||
|
return jsonify({"error": "Agent 不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=agent.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此 Agent"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = agent_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_agent = AgentService.update_agent(agent, data)
|
||||||
|
return jsonify(agent_schema.dump(updated_agent))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_agent(agent_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
agent = Agent.query.filter_by(id=agent_id).first()
|
||||||
|
if not agent:
|
||||||
|
return jsonify({"error": "Agent 不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=agent.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此 Agent"}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
AgentService.delete_agent(agent)
|
||||||
|
return jsonify({"message": "Agent 已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from flask import request, jsonify
|
||||||
|
from flask_jwt_extended import create_access_token
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import User
|
||||||
|
from ..schemas import UserCreateSchema
|
||||||
|
from ..services import AuthService
|
||||||
|
|
||||||
|
|
||||||
|
user_create_schema = UserCreateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/auth/register", methods=["POST"])
|
||||||
|
def register():
|
||||||
|
data = request.get_json()
|
||||||
|
errors = user_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = AuthService.register(
|
||||||
|
data["username"], data["password"], data.get("email")
|
||||||
|
)
|
||||||
|
access_token = create_access_token(identity=user.id)
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"message": "注册成功",
|
||||||
|
"user": user.to_dict(),
|
||||||
|
"access_token": access_token,
|
||||||
|
}
|
||||||
|
), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/auth/login", methods=["POST"])
|
||||||
|
def login():
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get("username")
|
||||||
|
password = data.get("password")
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return jsonify({"error": "用户名和密码不能为空"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = AuthService.login(username, password)
|
||||||
|
access_token = create_access_token(identity=user.id)
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"message": "登录成功",
|
||||||
|
"user": user.to_dict(),
|
||||||
|
"access_token": access_token,
|
||||||
|
}
|
||||||
|
), 200
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 401
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Channel
|
||||||
|
from ..schemas import ChannelSchema, ChannelCreateSchema, ChannelUpdateSchema
|
||||||
|
from ..services import ChannelService
|
||||||
|
|
||||||
|
|
||||||
|
channel_schema = ChannelSchema()
|
||||||
|
channel_create_schema = ChannelCreateSchema()
|
||||||
|
channel_update_schema = ChannelUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/channels", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_channels():
|
||||||
|
channels = Channel.query.all()
|
||||||
|
return jsonify(channel_schema.dump(channels, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/channels", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_channel():
|
||||||
|
data = request.get_json()
|
||||||
|
errors = channel_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel = ChannelService.create_channel(data)
|
||||||
|
return jsonify(channel_schema.dump(channel)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/channels/<int:channel_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_channel(channel_id):
|
||||||
|
channel = Channel.query.get(channel_id)
|
||||||
|
if not channel:
|
||||||
|
return jsonify({"error": "渠道不存在"}), 404
|
||||||
|
return jsonify(channel_schema.dump(channel))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/channels/<int:channel_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_channel(channel_id):
|
||||||
|
channel = Channel.query.get(channel_id)
|
||||||
|
if not channel:
|
||||||
|
return jsonify({"error": "渠道不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = channel_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_channel = ChannelService.update_channel(channel, data)
|
||||||
|
return jsonify(channel_schema.dump(updated_channel))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/channels/<int:channel_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_channel(channel_id):
|
||||||
|
try:
|
||||||
|
ChannelService.delete_channel(channel_id)
|
||||||
|
return jsonify({"message": "渠道已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Conversation, Workspace
|
||||||
|
from ..schemas import (
|
||||||
|
ConversationSchema,
|
||||||
|
ConversationCreateSchema,
|
||||||
|
ConversationUpdateSchema,
|
||||||
|
)
|
||||||
|
from ..services import ConversationService
|
||||||
|
|
||||||
|
|
||||||
|
conversation_schema = ConversationSchema()
|
||||||
|
conversation_create_schema = ConversationCreateSchema()
|
||||||
|
conversation_update_schema = ConversationUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/conversations", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_conversations(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
conversations = Conversation.query.filter_by(workspace_id=workspace_id).all()
|
||||||
|
return jsonify(conversation_schema.dump(conversations, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/conversations", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_conversation(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = conversation_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
conversation = ConversationService.create_conversation(workspace_id, data)
|
||||||
|
return jsonify(conversation_schema.dump(conversation)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/conversations/<int:conversation_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_conversation(conversation_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
conversation = Conversation.query.filter_by(id=conversation_id).first()
|
||||||
|
if not conversation:
|
||||||
|
return jsonify({"error": "会话不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=conversation.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此会话"}), 403
|
||||||
|
|
||||||
|
return jsonify(conversation_schema.dump(conversation))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/conversations/<int:conversation_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_conversation(conversation_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
conversation = Conversation.query.filter_by(id=conversation_id).first()
|
||||||
|
if not conversation:
|
||||||
|
return jsonify({"error": "会话不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=conversation.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此会话"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = conversation_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_conversation = ConversationService.update_conversation(
|
||||||
|
conversation, data
|
||||||
|
)
|
||||||
|
return jsonify(conversation_schema.dump(updated_conversation))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import CronJob
|
||||||
|
from ..schemas import CronJobSchema, CronJobCreateSchema, CronJobUpdateSchema
|
||||||
|
from ..services import CronJobService
|
||||||
|
|
||||||
|
|
||||||
|
cron_job_schema = CronJobSchema()
|
||||||
|
cron_job_create_schema = CronJobCreateSchema()
|
||||||
|
cron_job_update_schema = CronJobUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>/cron-jobs", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_cron_jobs(agent_id):
|
||||||
|
cron_jobs = CronJob.query.filter_by(agent_id=agent_id).all()
|
||||||
|
return jsonify(cron_job_schema.dump(cron_jobs, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>/cron-jobs", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_cron_job(agent_id):
|
||||||
|
data = request.get_json()
|
||||||
|
errors = cron_job_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
cron_job = CronJobService.create_cron_job(agent_id, data)
|
||||||
|
return jsonify(cron_job_schema.dump(cron_job)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/cron-jobs/<int:cron_job_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_cron_job(cron_job_id):
|
||||||
|
cron_job = CronJob.query.get(cron_job_id)
|
||||||
|
if not cron_job:
|
||||||
|
return jsonify({"error": "定时任务不存在"}), 404
|
||||||
|
return jsonify(cron_job_schema.dump(cron_job))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/cron-jobs/<int:cron_job_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_cron_job(cron_job_id):
|
||||||
|
cron_job = CronJob.query.get(cron_job_id)
|
||||||
|
if not cron_job:
|
||||||
|
return jsonify({"error": "定时任务不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = cron_job_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_cron_job = CronJobService.update_cron_job(cron_job, data)
|
||||||
|
return jsonify(cron_job_schema.dump(updated_cron_job))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/cron-jobs/<int:cron_job_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_cron_job(cron_job_id):
|
||||||
|
try:
|
||||||
|
CronJobService.delete_cron_job(cron_job_id)
|
||||||
|
return jsonify({"message": "定时任务已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Memory, Workspace
|
||||||
|
from ..schemas import MemorySchema, MemoryCreateSchema, MemoryUpdateSchema
|
||||||
|
from ..services import MemoryService
|
||||||
|
|
||||||
|
|
||||||
|
memory_schema = MemorySchema()
|
||||||
|
memory_create_schema = MemoryCreateSchema()
|
||||||
|
memory_update_schema = MemoryUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/memories", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_memories(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
memories = Memory.query.filter_by(workspace_id=workspace_id).all()
|
||||||
|
return jsonify(memory_schema.dump(memories, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>/memories", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_memory(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = memory_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
memory = MemoryService.create_memory(workspace_id, data)
|
||||||
|
return jsonify(memory_schema.dump(memory)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/memories/<int:memory_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_memory(memory_id):
|
||||||
|
memory = Memory.query.get(memory_id)
|
||||||
|
if not memory:
|
||||||
|
return jsonify({"error": "记忆不存在"}), 404
|
||||||
|
return jsonify(memory_schema.dump(memory))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/memories/<int:memory_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_memory(memory_id):
|
||||||
|
memory = Memory.query.get(memory_id)
|
||||||
|
if not memory:
|
||||||
|
return jsonify({"error": "记忆不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = memory_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_memory = MemoryService.update_memory(memory, data)
|
||||||
|
return jsonify(memory_schema.dump(updated_memory))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/memories/<int:memory_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_memory(memory_id):
|
||||||
|
try:
|
||||||
|
MemoryService.delete_memory(memory_id)
|
||||||
|
return jsonify({"message": "记忆已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Message, Conversation, Workspace
|
||||||
|
from ..schemas import MessageSchema, MessageCreateSchema
|
||||||
|
from ..services import MessageService
|
||||||
|
|
||||||
|
|
||||||
|
message_schema = MessageSchema()
|
||||||
|
message_create_schema = MessageCreateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/conversations/<int:conversation_id>/messages", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_messages(conversation_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
conversation = Conversation.query.filter_by(id=conversation_id).first()
|
||||||
|
if not conversation:
|
||||||
|
return jsonify({"error": "会话不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=conversation.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此会话"}), 403
|
||||||
|
|
||||||
|
messages = (
|
||||||
|
Message.query.filter_by(conversation_id=conversation_id)
|
||||||
|
.order_by(Message.created_at)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return jsonify(message_schema.dump(messages, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/conversations/<int:conversation_id>/messages", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_message(conversation_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
conversation = Conversation.query.filter_by(id=conversation_id).first()
|
||||||
|
if not conversation:
|
||||||
|
return jsonify({"error": "会话不存在"}), 404
|
||||||
|
|
||||||
|
workspace = Workspace.query.filter_by(
|
||||||
|
id=conversation.workspace_id, user_id=user_id
|
||||||
|
).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "无权访问此会话"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = message_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = MessageService.create_message(conversation_id, data)
|
||||||
|
return jsonify(message_schema.dump(message)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Model
|
||||||
|
from ..schemas import ModelSchema, ModelCreateSchema
|
||||||
|
from ..services import ModelService
|
||||||
|
|
||||||
|
|
||||||
|
model_schema = ModelSchema()
|
||||||
|
model_create_schema = ModelCreateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_models():
|
||||||
|
models = Model.query.all()
|
||||||
|
return jsonify(model_schema.dump(models, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_model():
|
||||||
|
data = request.get_json()
|
||||||
|
errors = model_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = ModelService.create_model(data)
|
||||||
|
return jsonify(model_schema.dump(model)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models/<int:model_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_model(model_id):
|
||||||
|
model = Model.query.get(model_id)
|
||||||
|
if not model:
|
||||||
|
return jsonify({"error": "模型不存在"}), 404
|
||||||
|
return jsonify(model_schema.dump(model))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models/<int:model_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_model(model_id):
|
||||||
|
model = Model.query.get(model_id)
|
||||||
|
if not model:
|
||||||
|
return jsonify({"error": "模型不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
try:
|
||||||
|
updated_model = ModelService.update_model(model, data)
|
||||||
|
return jsonify(model_schema.dump(updated_model))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models/<int:model_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_model(model_id):
|
||||||
|
try:
|
||||||
|
ModelService.delete_model(model_id)
|
||||||
|
return jsonify({"message": "模型已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/models/default", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_default_model():
|
||||||
|
model = Model.query.filter_by(is_default=True, is_active=True).first()
|
||||||
|
if not model:
|
||||||
|
return jsonify({"error": "默认模型不存在"}), 404
|
||||||
|
return jsonify(model_schema.dump(model))
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from flask import jsonify
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Skill
|
||||||
|
from ..schemas import SkillSchema
|
||||||
|
from ..services import SkillService
|
||||||
|
|
||||||
|
|
||||||
|
skill_schema = SkillSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/skills", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_skills():
|
||||||
|
skills = Skill.query.all()
|
||||||
|
return jsonify(skill_schema.dump(skills, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/skills/<int:skill_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_skill(skill_id):
|
||||||
|
skill = Skill.query.get(skill_id)
|
||||||
|
if not skill:
|
||||||
|
return jsonify({"error": "技能不存在"}), 404
|
||||||
|
return jsonify(skill_schema.dump(skill))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/skills/<int:skill_id>/install", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def install_skill(skill_id):
|
||||||
|
try:
|
||||||
|
SkillService.install_skill(skill_id)
|
||||||
|
return jsonify({"message": "技能已安装"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/skills/<int:skill_id>/uninstall", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def uninstall_skill(skill_id):
|
||||||
|
try:
|
||||||
|
SkillService.uninstall_skill(skill_id)
|
||||||
|
return jsonify({"message": "技能已卸载"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Tool, AgentTool
|
||||||
|
from ..schemas import (
|
||||||
|
ToolSchema,
|
||||||
|
ToolCreateSchema,
|
||||||
|
AgentToolSchema,
|
||||||
|
AgentToolCreateSchema,
|
||||||
|
)
|
||||||
|
from ..services import ToolService
|
||||||
|
|
||||||
|
|
||||||
|
tool_schema = ToolSchema()
|
||||||
|
tool_create_schema = ToolCreateSchema()
|
||||||
|
agent_tool_schema = AgentToolSchema()
|
||||||
|
agent_tool_create_schema = AgentToolCreateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/tools", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_tools():
|
||||||
|
tools = Tool.query.all()
|
||||||
|
return jsonify(tool_schema.dump(tools, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/tools", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_tool():
|
||||||
|
data = request.get_json()
|
||||||
|
errors = tool_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
tool = ToolService.create_tool(data)
|
||||||
|
return jsonify(tool_schema.dump(tool)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/tools/<int:tool_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_tool(tool_id):
|
||||||
|
tool = Tool.query.get(tool_id)
|
||||||
|
if not tool:
|
||||||
|
return jsonify({"error": "工具不存在"}), 404
|
||||||
|
return jsonify(tool_schema.dump(tool))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>/tools", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_agent_tools(agent_id):
|
||||||
|
agent_tools = AgentTool.query.filter_by(agent_id=agent_id).all()
|
||||||
|
return jsonify(agent_tool_schema.dump(agent_tools, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>/tools", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def add_agent_tool(agent_id):
|
||||||
|
data = request.get_json()
|
||||||
|
errors = agent_tool_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent_tool = ToolService.add_tool_to_agent(agent_id, data["tool_id"])
|
||||||
|
return jsonify(agent_tool_schema.dump(agent_tool)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/agents/<int:agent_id>/tools/<int:tool_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def remove_agent_tool(agent_id, tool_id):
|
||||||
|
try:
|
||||||
|
ToolService.remove_tool_from_agent(agent_id, tool_id)
|
||||||
|
return jsonify({"message": "工具已移除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import User
|
||||||
|
from ..schemas import UserSchema, UserUpdateSchema
|
||||||
|
from ..services import UserService
|
||||||
|
|
||||||
|
|
||||||
|
user_schema = UserSchema()
|
||||||
|
user_update_schema = UserUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/users/me", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_current_user():
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return jsonify({"error": "用户不存在"}), 404
|
||||||
|
return jsonify(user_schema.dump(user))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/users/me", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_current_user():
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return jsonify({"error": "用户不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = user_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_user = UserService.update_user(user, data)
|
||||||
|
return jsonify(user_schema.dump(updated_user))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
from . import api_bp
|
||||||
|
from ..models import Workspace
|
||||||
|
from ..schemas import WorkspaceSchema, WorkspaceCreateSchema, WorkspaceUpdateSchema
|
||||||
|
from ..services import WorkspaceService
|
||||||
|
|
||||||
|
|
||||||
|
workspace_schema = WorkspaceSchema()
|
||||||
|
workspace_create_schema = WorkspaceCreateSchema()
|
||||||
|
workspace_update_schema = WorkspaceUpdateSchema()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_workspaces():
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspaces = Workspace.query.filter_by(user_id=user_id).all()
|
||||||
|
return jsonify(workspace_schema.dump(workspaces, many=True))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def create_workspace():
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
data = request.get_json()
|
||||||
|
errors = workspace_create_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
workspace = WorkspaceService.create_workspace(
|
||||||
|
user_id, data["name"], data.get("description")
|
||||||
|
)
|
||||||
|
return jsonify(workspace_schema.dump(workspace)), 201
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_workspace(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
return jsonify(workspace_schema.dump(workspace))
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>", methods=["PUT"])
|
||||||
|
@jwt_required()
|
||||||
|
def update_workspace(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
errors = workspace_update_schema.validate(data)
|
||||||
|
if errors:
|
||||||
|
return jsonify({"error": errors}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_workspace = WorkspaceService.update_workspace(workspace, data)
|
||||||
|
return jsonify(workspace_schema.dump(updated_workspace))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/workspaces/<int:workspace_id>", methods=["DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_workspace(workspace_id):
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
workspace = Workspace.query.filter_by(id=workspace_id, user_id=user_id).first()
|
||||||
|
if not workspace:
|
||||||
|
return jsonify({"error": "工作空间不存在"}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
WorkspaceService.delete_workspace(workspace)
|
||||||
|
return jsonify({"message": "工作空间已删除"})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from .web import WebChannel
|
||||||
|
from .dingtalk import DingTalkChannel
|
||||||
|
from .feishu import FeishuChannel
|
||||||
|
from .telegram import TelegramChannel
|
||||||
|
from .discord import DiscordChannel
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"WebChannel",
|
||||||
|
"DingTalkChannel",
|
||||||
|
"FeishuChannel",
|
||||||
|
"TelegramChannel",
|
||||||
|
"DiscordChannel",
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class DingTalkChannel:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def send_message(self, user_id, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive_message(self, data):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class DiscordChannel:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def send_message(self, user_id, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive_message(self, data):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class FeishuChannel:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def send_message(self, user_id, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive_message(self, data):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class TelegramChannel:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def send_message(self, user_id, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive_message(self, data):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class WebChannel:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def send_message(self, user_id, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive_message(self, data):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY") or "dev-secret-key-change-in-production"
|
||||||
|
|
||||||
|
Flask_ENV = os.environ.get("FLASK_ENV") or "development"
|
||||||
|
Flask_DEBUG = os.environ.get("FLASK_DEBUG", "True").lower() == "true"
|
||||||
|
Flask_HOST = os.environ.get("FLASK_HOST", "0.0.0.0")
|
||||||
|
Flask_PORT = int(os.environ.get("FLASK_PORT", 5000))
|
||||||
|
|
||||||
|
DATABASE_URL = (
|
||||||
|
os.environ.get("DATABASE_URL")
|
||||||
|
or "mysql+pymysql://sentclaw:sentclaw123@localhost:3306/sentclaw"
|
||||||
|
)
|
||||||
|
|
||||||
|
REDIS_URL = os.environ.get("REDIS_URL") or "redis://:redis123@localhost:6379/0"
|
||||||
|
|
||||||
|
JWT_SECRET_KEY = (
|
||||||
|
os.environ.get("JWT_SECRET_KEY") or "jwt-secret-key-change-in-production"
|
||||||
|
)
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRES = timedelta(
|
||||||
|
seconds=int(os.environ.get("JWT_ACCESS_TOKEN_EXPIRES", 3600))
|
||||||
|
)
|
||||||
|
|
||||||
|
CORS_ORIGINS = os.environ.get(
|
||||||
|
"CORS_ORIGINS", "http://localhost:5173,http://localhost:3000"
|
||||||
|
).split(",")
|
||||||
|
|
||||||
|
UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "uploads")
|
||||||
|
MAX_CONTENT_LENGTH = int(os.environ.get("MAX_CONTENT_LENGTH", 16777216))
|
||||||
|
|
||||||
|
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
|
LOG_FILE = os.environ.get("LOG_FILE", "logs/app.log")
|
||||||
|
|
||||||
|
LLM_TIMEOUT = 30
|
||||||
|
LLM_MAX_RETRIES = 3
|
||||||
|
|
||||||
|
MCP_TIMEOUT = 10
|
||||||
|
|
||||||
|
AGENT_MAX_ITERATIONS = 10
|
||||||
|
AGENT_THINKING_TIMEOUT = 60
|
||||||
|
|
||||||
|
MEMORY_WINDOW_SIZE = 10
|
||||||
|
MEMORY_COMPRESSION_THRESHOLD = 0.8
|
||||||
|
|
||||||
|
|
||||||
|
class DevelopmentConfig(Config):
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionConfig(Config):
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
|
class TestingConfig(Config):
|
||||||
|
TESTING = True
|
||||||
|
DATABASE_URL = "sqlite:///:memory:"
|
||||||
|
|
||||||
|
|
||||||
|
config_by_name = {
|
||||||
|
"development": DevelopmentConfig,
|
||||||
|
"production": ProductionConfig,
|
||||||
|
"testing": TestingConfig,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from .factory import LLMFactory
|
||||||
|
from .base import BaseLLMClient
|
||||||
|
from .dashscope import DashScopeClient
|
||||||
|
from .openai import OpenAIClient
|
||||||
|
from .anthropic import AnthropicClient
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LLMFactory",
|
||||||
|
"BaseLLMClient",
|
||||||
|
"DashScopeClient",
|
||||||
|
"OpenAIClient",
|
||||||
|
"AnthropicClient",
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .base import BaseLLMClient
|
||||||
|
|
||||||
|
|
||||||
|
class AnthropicClient(BaseLLMClient):
|
||||||
|
def chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stream_chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLLMClient(ABC):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stream_chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .base import BaseLLMClient
|
||||||
|
|
||||||
|
|
||||||
|
class DashScopeClient(BaseLLMClient):
|
||||||
|
def chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stream_chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from .dashscope import DashScopeClient
|
||||||
|
from .openai import OpenAIClient
|
||||||
|
from .anthropic import AnthropicClient
|
||||||
|
|
||||||
|
|
||||||
|
class LLMFactory:
|
||||||
|
_clients = {
|
||||||
|
"dashscope": DashScopeClient,
|
||||||
|
"openai": OpenAIClient,
|
||||||
|
"anthropic": AnthropicClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_client(cls, provider, config):
|
||||||
|
client_class = cls._clients.get(provider)
|
||||||
|
if not client_class:
|
||||||
|
raise ValueError(f"不支持的 LLM 提供商: {provider}")
|
||||||
|
return client_class(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_client(cls, provider, client_class):
|
||||||
|
cls._clients[provider] = client_class
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .base import BaseLLMClient
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIClient(BaseLLMClient):
|
||||||
|
def chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stream_chat(self, messages, **kwargs):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .manager import MemoryManager
|
||||||
|
from .extractor import MemoryExtractor
|
||||||
|
from .integrator import MemoryIntegrator
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["MemoryManager", "MemoryExtractor", "MemoryIntegrator"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class MemoryExtractor:
|
||||||
|
def __init__(self, llm_client):
|
||||||
|
self.llm_client = llm_client
|
||||||
|
|
||||||
|
def extract_from_conversation(self, conversation):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class MemoryIntegrator:
|
||||||
|
def integrate_memories(self, memories):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class MemoryManager:
|
||||||
|
def __init__(self, workspace_id):
|
||||||
|
self.workspace_id = workspace_id
|
||||||
|
self.memories = []
|
||||||
|
|
||||||
|
def add_memory(self, content, memory_type="short", tags=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def retrieve_memories(self, query=None, memory_type=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compress_memories(self):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
from .user import User
|
||||||
|
from .workspace import Workspace
|
||||||
|
from .agent import Agent
|
||||||
|
from .conversation import Conversation
|
||||||
|
from .message import Message
|
||||||
|
from .tool import Tool, AgentTool
|
||||||
|
from .skill import Skill
|
||||||
|
from .memory import Memory
|
||||||
|
from .model import Model
|
||||||
|
from .cron_job import CronJob
|
||||||
|
from .channel import Channel
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(BaseModel):
|
||||||
|
__tablename__ = "agents"
|
||||||
|
|
||||||
|
workspace_id = db.Column(
|
||||||
|
db.BigInteger,
|
||||||
|
db.ForeignKey("workspaces.id"),
|
||||||
|
nullable=False,
|
||||||
|
comment="工作空间ID",
|
||||||
|
)
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment="Agent 名称")
|
||||||
|
description = db.Column(db.Text, nullable=True, comment="描述")
|
||||||
|
system_prompt = db.Column(db.Text, nullable=True, comment="系统提示词")
|
||||||
|
model_id = db.Column(db.String(50), nullable=True, comment="模型ID")
|
||||||
|
temperature = db.Column(
|
||||||
|
db.Numeric(3, 2), default=0.70, nullable=False, comment="温度参数"
|
||||||
|
)
|
||||||
|
max_tokens = db.Column(
|
||||||
|
db.Integer, default=2000, nullable=False, comment="最大Token数"
|
||||||
|
)
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
|
|
||||||
|
conversations = db.relationship(
|
||||||
|
"Conversation", backref="agent", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
tools = db.relationship(
|
||||||
|
"AgentTool", backref="agent", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
cron_jobs = db.relationship(
|
||||||
|
"CronJob", backref="agent", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
memories = db.relationship(
|
||||||
|
"Memory", backref="agent", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(db.Model):
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True, comment="ID")
|
||||||
|
created_at = db.Column(
|
||||||
|
db.DateTime, default=datetime.utcnow, nullable=False, comment="创建时间"
|
||||||
|
)
|
||||||
|
updated_at = db.Column(
|
||||||
|
db.DateTime,
|
||||||
|
default=datetime.utcnow,
|
||||||
|
onupdate=datetime.utcnow,
|
||||||
|
nullable=False,
|
||||||
|
comment="更新时间",
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
db.session.delete(self)
|
||||||
|
db.session.commit()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(BaseModel):
|
||||||
|
__tablename__ = "channels"
|
||||||
|
|
||||||
|
type = db.Column(
|
||||||
|
db.String(50),
|
||||||
|
nullable=False,
|
||||||
|
comment="类型(web, dingtalk, feishu, telegram, discord, qq)",
|
||||||
|
)
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment="渠道名称")
|
||||||
|
config = db.Column(db.JSON, nullable=False, comment="配置(JSON)")
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Conversation(BaseModel):
|
||||||
|
__tablename__ = "conversations"
|
||||||
|
|
||||||
|
workspace_id = db.Column(
|
||||||
|
db.BigInteger,
|
||||||
|
db.ForeignKey("workspaces.id"),
|
||||||
|
nullable=False,
|
||||||
|
comment="工作空间ID",
|
||||||
|
)
|
||||||
|
agent_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("agents.id"), nullable=False, comment="Agent ID"
|
||||||
|
)
|
||||||
|
title = db.Column(db.String(200), nullable=True, comment="会话标题")
|
||||||
|
channel = db.Column(
|
||||||
|
db.String(50),
|
||||||
|
default="web",
|
||||||
|
nullable=False,
|
||||||
|
comment="渠道(web, dingtalk, feishu, telegram, discord)",
|
||||||
|
)
|
||||||
|
channel_user_id = db.Column(db.String(100), nullable=True, comment="渠道用户ID")
|
||||||
|
status = db.Column(
|
||||||
|
db.String(20),
|
||||||
|
default="active",
|
||||||
|
nullable=False,
|
||||||
|
comment="状态(active, archived, deleted)",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = db.relationship(
|
||||||
|
"Message", backref="conversation", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class CronJob(BaseModel):
|
||||||
|
__tablename__ = "cron_jobs"
|
||||||
|
|
||||||
|
agent_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("agents.id"), nullable=False, comment="Agent ID"
|
||||||
|
)
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment="任务名称")
|
||||||
|
cron_expression = db.Column(db.String(50), nullable=False, comment="Cron 表达式")
|
||||||
|
prompt = db.Column(db.Text, nullable=False, comment="执行提示词")
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
|
last_run_at = db.Column(db.DateTime, nullable=True, comment="上次运行时间")
|
||||||
|
next_run_at = db.Column(db.DateTime, nullable=True, comment="下次运行时间")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Memory(BaseModel):
|
||||||
|
__tablename__ = "memories"
|
||||||
|
|
||||||
|
workspace_id = db.Column(
|
||||||
|
db.BigInteger,
|
||||||
|
db.ForeignKey("workspaces.id"),
|
||||||
|
nullable=False,
|
||||||
|
comment="工作空间ID",
|
||||||
|
)
|
||||||
|
agent_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("agents.id"), nullable=True, comment="Agent ID"
|
||||||
|
)
|
||||||
|
type = db.Column(
|
||||||
|
db.String(50), nullable=False, comment="类型(profile, memory, note)"
|
||||||
|
)
|
||||||
|
content = db.Column(db.Text, nullable=False, comment="内容")
|
||||||
|
tags = db.Column(db.JSON, nullable=True, comment="标签")
|
||||||
|
importance = db.Column(
|
||||||
|
db.Integer, default=5, nullable=False, comment="重要性(1-10)"
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import json
|
||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Message(BaseModel):
|
||||||
|
__tablename__ = "messages"
|
||||||
|
|
||||||
|
conversation_id = db.Column(
|
||||||
|
db.BigInteger,
|
||||||
|
db.ForeignKey("conversations.id"),
|
||||||
|
nullable=False,
|
||||||
|
comment="会话ID",
|
||||||
|
)
|
||||||
|
role = db.Column(
|
||||||
|
db.String(20), nullable=False, comment="角色(user, assistant, system)"
|
||||||
|
)
|
||||||
|
content = db.Column(db.Text, nullable=False, comment="消息内容")
|
||||||
|
tokens = db.Column(db.Integer, nullable=True, comment="Token数")
|
||||||
|
model = db.Column(db.String(50), nullable=True, comment="使用的模型")
|
||||||
|
tool_calls = db.Column(db.JSON, nullable=True, comment="工具调用记录")
|
||||||
|
metadata = db.Column(db.JSON, nullable=True, comment="元数据")
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
result = super().to_dict()
|
||||||
|
if result.get("tool_calls"):
|
||||||
|
result["tool_calls"] = (
|
||||||
|
json.loads(result["tool_calls"])
|
||||||
|
if isinstance(result["tool_calls"], str)
|
||||||
|
else result["tool_calls"]
|
||||||
|
)
|
||||||
|
if result.get("metadata"):
|
||||||
|
result["metadata"] = (
|
||||||
|
json.loads(result["metadata"])
|
||||||
|
if isinstance(result["metadata"], str)
|
||||||
|
else result["metadata"]
|
||||||
|
)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Model(BaseModel):
|
||||||
|
__tablename__ = 'models'
|
||||||
|
|
||||||
|
provider = db.Column(db.String(50), nullable=False, comment='提供商(dashscope, openai, anthropic, etc)')
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment='模型名称')
|
||||||
|
model_id = db.Column(db.String(100), nullable=False, comment='模型ID(如 qwen-plus)')
|
||||||
|
api_key = db.Column(db.String(255), nullable=True, comment='API Key')
|
||||||
|
base_url = db.Column(db.String(255), nullable=True, comment='API Base URL')
|
||||||
|
is_default = db.Column(db.Boolean, default=False, nullable=False, comment='是否默认')
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment='是否激活')
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Skill(BaseModel):
|
||||||
|
__tablename__ = "skills"
|
||||||
|
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment="技能名称")
|
||||||
|
version = db.Column(db.String(20), default="1.0.0", nullable=False, comment="版本")
|
||||||
|
description = db.Column(db.Text, nullable=True, comment="描述")
|
||||||
|
author = db.Column(db.String(100), nullable=True, comment="作者")
|
||||||
|
repository = db.Column(db.String(255), nullable=True, comment="仓库地址")
|
||||||
|
config = db.Column(db.JSON, nullable=True, comment="配置(JSON)")
|
||||||
|
is_installed = db.Column(
|
||||||
|
db.Boolean, default=False, nullable=False, comment="是否已安装"
|
||||||
|
)
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Tool(BaseModel):
|
||||||
|
__tablename__ = "tools"
|
||||||
|
|
||||||
|
name = db.Column(db.String(100), unique=True, nullable=False, comment="工具名称")
|
||||||
|
type = db.Column(
|
||||||
|
db.String(50), nullable=False, comment="类型(builtin, mcp, custom)"
|
||||||
|
)
|
||||||
|
description = db.Column(db.Text, nullable=True, comment="描述")
|
||||||
|
config = db.Column(db.JSON, nullable=True, comment="配置(JSON)")
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
|
|
||||||
|
agent_tools = db.relationship(
|
||||||
|
"AgentTool", backref="tool", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentTool(BaseModel):
|
||||||
|
__tablename__ = "agent_tools"
|
||||||
|
|
||||||
|
agent_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("agents.id"), nullable=False, comment="Agent ID"
|
||||||
|
)
|
||||||
|
tool_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("tools.id"), nullable=False, comment="工具ID"
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
username = db.Column(db.String(50), unique=True, nullable=False, comment="用户名")
|
||||||
|
password = db.Column(db.String(255), nullable=False, comment="密码(加密)")
|
||||||
|
email = db.Column(db.String(100), unique=True, nullable=True, comment="邮箱")
|
||||||
|
avatar = db.Column(db.String(255), nullable=True, comment="头像URL")
|
||||||
|
is_active = db.Column(db.Boolean, default=True, nullable=False, comment="是否激活")
|
||||||
|
is_admin = db.Column(
|
||||||
|
db.Boolean, default=False, nullable=False, comment="是否管理员"
|
||||||
|
)
|
||||||
|
|
||||||
|
workspaces = db.relationship(
|
||||||
|
"Workspace", backref="user", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from .base import BaseModel
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class Workspace(BaseModel):
|
||||||
|
__tablename__ = "workspaces"
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.BigInteger, db.ForeignKey("users.id"), nullable=False, comment="用户ID"
|
||||||
|
)
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment="工作空间名称")
|
||||||
|
description = db.Column(db.Text, nullable=True, comment="描述")
|
||||||
|
is_default = db.Column(
|
||||||
|
db.Boolean, default=False, nullable=False, comment="是否默认"
|
||||||
|
)
|
||||||
|
|
||||||
|
agents = db.relationship(
|
||||||
|
"Agent", backref="workspace", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
conversations = db.relationship(
|
||||||
|
"Conversation", backref="workspace", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
memories = db.relationship(
|
||||||
|
"Memory", backref="workspace", lazy=True, cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .user import UserSchema
|
||||||
|
from .workspace import WorkspaceSchema
|
||||||
|
from .agent import AgentSchema
|
||||||
|
from .conversation import ConversationSchema
|
||||||
|
from .message import MessageSchema
|
||||||
|
from .tool import ToolSchema, AgentToolSchema
|
||||||
|
from .skill import SkillSchema
|
||||||
|
from .memory import MemorySchema
|
||||||
|
from .model import ModelSchema
|
||||||
|
from .cron_job import CronJobSchema
|
||||||
|
from .channel import ChannelSchema
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AgentSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
workspace_id = fields.Integer()
|
||||||
|
name = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
system_prompt = fields.String(allow_none=True)
|
||||||
|
model_id = fields.String(allow_none=True)
|
||||||
|
temperature = fields.Float()
|
||||||
|
max_tokens = fields.Integer()
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentCreateSchema(Schema):
|
||||||
|
name = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
system_prompt = fields.String(allow_none=True)
|
||||||
|
model_id = fields.String(allow_none=True)
|
||||||
|
temperature = fields.Float()
|
||||||
|
max_tokens = fields.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
class AgentUpdateSchema(Schema):
|
||||||
|
name = fields.String()
|
||||||
|
description = fields.String()
|
||||||
|
system_prompt = fields.String()
|
||||||
|
model_id = fields.String()
|
||||||
|
temperature = fields.Float()
|
||||||
|
max_tokens = fields.Integer()
|
||||||
|
is_active = fields.Boolean()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
type = fields.String(required=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
config = fields.Dict(required=True, load_only=True)
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelCreateSchema(Schema):
|
||||||
|
type = fields.String(required=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
config = fields.Dict(required=True, load_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelUpdateSchema(Schema):
|
||||||
|
name = fields.String()
|
||||||
|
config = fields.Dict(load_only=True)
|
||||||
|
is_active = fields.Boolean()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
workspace_id = fields.Integer()
|
||||||
|
agent_id = fields.Integer()
|
||||||
|
title = fields.String(allow_none=True)
|
||||||
|
channel = fields.String()
|
||||||
|
channel_user_id = fields.String(allow_none=True)
|
||||||
|
status = fields.String()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationCreateSchema(Schema):
|
||||||
|
agent_id = fields.Integer(required=True)
|
||||||
|
title = fields.String(allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationUpdateSchema(Schema):
|
||||||
|
title = fields.String()
|
||||||
|
status = fields.String()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class CronJobSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
agent_id = fields.Integer()
|
||||||
|
name = fields.String(required=True)
|
||||||
|
cron_expression = fields.String(required=True)
|
||||||
|
prompt = fields.String(required=True)
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
last_run_at = fields.DateTime(allow_none=True)
|
||||||
|
next_run_at = fields.DateTime(allow_none=True)
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CronJobCreateSchema(Schema):
|
||||||
|
name = fields.String(required=True)
|
||||||
|
cron_expression = fields.String(required=True)
|
||||||
|
prompt = fields.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CronJobUpdateSchema(Schema):
|
||||||
|
name = fields.String()
|
||||||
|
cron_expression = fields.String()
|
||||||
|
prompt = fields.String()
|
||||||
|
is_active = fields.Boolean()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class MemorySchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
workspace_id = fields.Integer()
|
||||||
|
agent_id = fields.Integer(allow_none=True)
|
||||||
|
type = fields.String(required=True)
|
||||||
|
content = fields.String(required=True)
|
||||||
|
tags = fields.List(fields.String(), allow_none=True)
|
||||||
|
importance = fields.Integer()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCreateSchema(Schema):
|
||||||
|
type = fields.String(required=True)
|
||||||
|
content = fields.String(required=True)
|
||||||
|
tags = fields.List(fields.String(), allow_none=True)
|
||||||
|
importance = fields.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryUpdateSchema(Schema):
|
||||||
|
content = fields.String()
|
||||||
|
tags = fields.List(fields.String())
|
||||||
|
importance = fields.Integer()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class MessageSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
conversation_id = fields.Integer()
|
||||||
|
role = fields.String(required=True)
|
||||||
|
content = fields.String(required=True)
|
||||||
|
tokens = fields.Integer(allow_none=True)
|
||||||
|
model = fields.String(allow_none=True)
|
||||||
|
tool_calls = fields.List(fields.Dict(), allow_none=True)
|
||||||
|
metadata = fields.Dict(allow_none=True)
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageCreateSchema(Schema):
|
||||||
|
role = fields.String(required=True)
|
||||||
|
content = fields.String(required=True)
|
||||||
|
tool_calls = fields.List(fields.Dict(), allow_none=True)
|
||||||
|
metadata = fields.Dict(allow_none=True)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
provider = fields.String(required=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
model_id = fields.String(required=True)
|
||||||
|
api_key = fields.String(allow_none=True, load_only=True)
|
||||||
|
base_url = fields.String(allow_none=True)
|
||||||
|
is_default = fields.Boolean()
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelCreateSchema(Schema):
|
||||||
|
provider = fields.String(required=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
model_id = fields.String(required=True)
|
||||||
|
api_key = fields.String(allow_none=True, load_only=True)
|
||||||
|
base_url = fields.String(allow_none=True)
|
||||||
|
is_default = fields.Boolean()
|
||||||
|
is_active = fields.Boolean()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class SkillSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
version = fields.String()
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
author = fields.String(allow_none=True)
|
||||||
|
repository = fields.String(allow_none=True)
|
||||||
|
config = fields.Dict(allow_none=True)
|
||||||
|
is_installed = fields.Boolean()
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ToolSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
name = fields.String(required=True)
|
||||||
|
type = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
config = fields.Dict(allow_none=True)
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCreateSchema(Schema):
|
||||||
|
name = fields.String(required=True)
|
||||||
|
type = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
config = fields.Dict(allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentToolSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
agent_id = fields.Integer()
|
||||||
|
tool_id = fields.Integer()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentToolCreateSchema(Schema):
|
||||||
|
tool_id = fields.Integer(required=True)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class UserSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
username = fields.String(required=True)
|
||||||
|
email = fields.String(allow_none=True)
|
||||||
|
avatar = fields.String(allow_none=True)
|
||||||
|
is_active = fields.Boolean()
|
||||||
|
is_admin = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreateSchema(Schema):
|
||||||
|
username = fields.String(required=True)
|
||||||
|
password = fields.String(required=True, load_only=True)
|
||||||
|
email = fields.String(allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateSchema(Schema):
|
||||||
|
email = fields.String(allow_none=True)
|
||||||
|
avatar = fields.String(allow_none=True)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceSchema(Schema):
|
||||||
|
id = fields.Integer(dump_only=True)
|
||||||
|
user_id = fields.Integer()
|
||||||
|
name = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
is_default = fields.Boolean()
|
||||||
|
created_at = fields.DateTime(dump_only=True)
|
||||||
|
updated_at = fields.DateTime(dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceCreateSchema(Schema):
|
||||||
|
name = fields.String(required=True)
|
||||||
|
description = fields.String(allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUpdateSchema(Schema):
|
||||||
|
name = fields.String()
|
||||||
|
description = fields.String()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from .auth import AuthService
|
||||||
|
from .user import UserService
|
||||||
|
from .workspace import WorkspaceService
|
||||||
|
from .agent import AgentService
|
||||||
|
from .conversation import ConversationService
|
||||||
|
from .message import MessageService
|
||||||
|
from .tool import ToolService
|
||||||
|
from .skill import SkillService
|
||||||
|
from .memory import MemoryService
|
||||||
|
from .model import ModelService
|
||||||
|
from .cron_job import CronJobService
|
||||||
|
from .channel import ChannelService
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from ..models import Agent
|
||||||
|
|
||||||
|
|
||||||
|
class AgentService:
|
||||||
|
@staticmethod
|
||||||
|
def create_agent(workspace_id, data):
|
||||||
|
agent = Agent(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
name=data["name"],
|
||||||
|
description=data.get("description"),
|
||||||
|
system_prompt=data.get("system_prompt"),
|
||||||
|
model_id=data.get("model_id"),
|
||||||
|
temperature=data.get("temperature", 0.70),
|
||||||
|
max_tokens=data.get("max_tokens", 2000),
|
||||||
|
)
|
||||||
|
agent.save()
|
||||||
|
return agent
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_agent(agent, data):
|
||||||
|
if "name" in data:
|
||||||
|
agent.name = data["name"]
|
||||||
|
|
||||||
|
if "description" in data:
|
||||||
|
agent.description = data["description"]
|
||||||
|
|
||||||
|
if "system_prompt" in data:
|
||||||
|
agent.system_prompt = data["system_prompt"]
|
||||||
|
|
||||||
|
if "model_id" in data:
|
||||||
|
agent.model_id = data["model_id"]
|
||||||
|
|
||||||
|
if "temperature" in data:
|
||||||
|
agent.temperature = data["temperature"]
|
||||||
|
|
||||||
|
if "max_tokens" in data:
|
||||||
|
agent.max_tokens = data["max_tokens"]
|
||||||
|
|
||||||
|
if "is_active" in data:
|
||||||
|
agent.is_active = data["is_active"]
|
||||||
|
|
||||||
|
agent.save()
|
||||||
|
return agent
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_agent(agent):
|
||||||
|
agent.delete()
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import bcrypt
|
||||||
|
from ..models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
@staticmethod
|
||||||
|
def register(username, password, email=None):
|
||||||
|
if User.query.filter_by(username=username).first():
|
||||||
|
raise ValueError("用户名已存在")
|
||||||
|
|
||||||
|
if email and User.query.filter_by(email=email).first():
|
||||||
|
raise ValueError("邮箱已被使用")
|
||||||
|
|
||||||
|
hashed_password = bcrypt.hashpw(
|
||||||
|
password.encode("utf-8"), bcrypt.gensalt()
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
user = User(username=username, password=hashed_password, email=email)
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login(username, password):
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if not user:
|
||||||
|
raise ValueError("用户名或密码错误")
|
||||||
|
|
||||||
|
if not bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")):
|
||||||
|
raise ValueError("用户名或密码错误")
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise ValueError("用户已被禁用")
|
||||||
|
|
||||||
|
return user
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from ..models import Channel
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelService:
|
||||||
|
@staticmethod
|
||||||
|
def create_channel(data):
|
||||||
|
channel = Channel(
|
||||||
|
type=data["type"],
|
||||||
|
name=data["name"],
|
||||||
|
config=data["config"],
|
||||||
|
is_active=data.get("is_active", True),
|
||||||
|
)
|
||||||
|
channel.save()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_channel(channel, data):
|
||||||
|
if "name" in data:
|
||||||
|
channel.name = data["name"]
|
||||||
|
|
||||||
|
if "config" in data:
|
||||||
|
channel.config = data["config"]
|
||||||
|
|
||||||
|
if "is_active" in data:
|
||||||
|
channel.is_active = data["is_active"]
|
||||||
|
|
||||||
|
channel.save()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_channel(channel_id):
|
||||||
|
channel = Channel.query.get(channel_id)
|
||||||
|
if not channel:
|
||||||
|
raise ValueError("渠道不存在")
|
||||||
|
channel.delete()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from ..models import Conversation
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationService:
|
||||||
|
@staticmethod
|
||||||
|
def create_conversation(workspace_id, data):
|
||||||
|
conversation = Conversation(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
agent_id=data["agent_id"],
|
||||||
|
title=data.get("title"),
|
||||||
|
)
|
||||||
|
conversation.save()
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_conversation(conversation, data):
|
||||||
|
if "title" in data:
|
||||||
|
conversation.title = data["title"]
|
||||||
|
|
||||||
|
if "status" in data:
|
||||||
|
conversation.status = data["status"]
|
||||||
|
|
||||||
|
conversation.save()
|
||||||
|
return conversation
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from ..models import CronJob
|
||||||
|
|
||||||
|
|
||||||
|
class CronJobService:
|
||||||
|
@staticmethod
|
||||||
|
def create_cron_job(agent_id, data):
|
||||||
|
cron_job = CronJob(
|
||||||
|
agent_id=agent_id,
|
||||||
|
name=data["name"],
|
||||||
|
cron_expression=data["cron_expression"],
|
||||||
|
prompt=data["prompt"],
|
||||||
|
is_active=data.get("is_active", True),
|
||||||
|
)
|
||||||
|
cron_job.save()
|
||||||
|
return cron_job
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_cron_job(cron_job, data):
|
||||||
|
if "name" in data:
|
||||||
|
cron_job.name = data["name"]
|
||||||
|
|
||||||
|
if "cron_expression" in data:
|
||||||
|
cron_job.cron_expression = data["cron_expression"]
|
||||||
|
|
||||||
|
if "prompt" in data:
|
||||||
|
cron_job.prompt = data["prompt"]
|
||||||
|
|
||||||
|
if "is_active" in data:
|
||||||
|
cron_job.is_active = data["is_active"]
|
||||||
|
|
||||||
|
cron_job.save()
|
||||||
|
return cron_job
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_cron_job(cron_job_id):
|
||||||
|
cron_job = CronJob.query.get(cron_job_id)
|
||||||
|
if not cron_job:
|
||||||
|
raise ValueError("定时任务不存在")
|
||||||
|
cron_job.delete()
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from ..models import Memory
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryService:
|
||||||
|
@staticmethod
|
||||||
|
def create_memory(workspace_id, data):
|
||||||
|
memory = Memory(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
agent_id=data.get("agent_id"),
|
||||||
|
type=data["type"],
|
||||||
|
content=data["content"],
|
||||||
|
tags=data.get("tags"),
|
||||||
|
importance=data.get("importance", 5),
|
||||||
|
)
|
||||||
|
memory.save()
|
||||||
|
return memory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_memory(memory, data):
|
||||||
|
if "content" in data:
|
||||||
|
memory.content = data["content"]
|
||||||
|
|
||||||
|
if "tags" in data:
|
||||||
|
memory.tags = data["tags"]
|
||||||
|
|
||||||
|
if "importance" in data:
|
||||||
|
memory.importance = data["importance"]
|
||||||
|
|
||||||
|
memory.save()
|
||||||
|
return memory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_memory(memory):
|
||||||
|
memory.delete()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from ..models import Message
|
||||||
|
|
||||||
|
|
||||||
|
class MessageService:
|
||||||
|
@staticmethod
|
||||||
|
def create_message(conversation_id, data):
|
||||||
|
message = Message(
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
role=data["role"],
|
||||||
|
content=data["content"],
|
||||||
|
tool_calls=data.get("tool_calls"),
|
||||||
|
metadata=data.get("metadata"),
|
||||||
|
)
|
||||||
|
message.save()
|
||||||
|
return message
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from ..models import Model
|
||||||
|
|
||||||
|
|
||||||
|
class ModelService:
|
||||||
|
@staticmethod
|
||||||
|
def create_model(data):
|
||||||
|
model = Model(
|
||||||
|
provider=data["provider"],
|
||||||
|
name=data["name"],
|
||||||
|
model_id=data["model_id"],
|
||||||
|
api_key=data.get("api_key"),
|
||||||
|
base_url=data.get("base_url"),
|
||||||
|
is_default=data.get("is_default", False),
|
||||||
|
is_active=data.get("is_active", True),
|
||||||
|
)
|
||||||
|
|
||||||
|
if model.is_default:
|
||||||
|
Model.query.filter_by(is_default=True).update({"is_default": False})
|
||||||
|
|
||||||
|
model.save()
|
||||||
|
return model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_model(model, data):
|
||||||
|
if "name" in data:
|
||||||
|
model.name = data["name"]
|
||||||
|
|
||||||
|
if "api_key" in data:
|
||||||
|
model.api_key = data["api_key"]
|
||||||
|
|
||||||
|
if "base_url" in data:
|
||||||
|
model.base_url = data["base_url"]
|
||||||
|
|
||||||
|
if "is_default" in data and data["is_default"]:
|
||||||
|
Model.query.filter(Model.id != model.id, Model.is_default == True).update(
|
||||||
|
{"is_default": False}
|
||||||
|
)
|
||||||
|
model.is_default = True
|
||||||
|
|
||||||
|
if "is_active" in data:
|
||||||
|
model.is_active = data["is_active"]
|
||||||
|
|
||||||
|
model.save()
|
||||||
|
return model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_model(model_id):
|
||||||
|
model = Model.query.get(model_id)
|
||||||
|
if model.is_default:
|
||||||
|
raise ValueError("默认模型不能删除")
|
||||||
|
model.delete()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from ..models import Skill
|
||||||
|
|
||||||
|
|
||||||
|
class SkillService:
|
||||||
|
@staticmethod
|
||||||
|
def install_skill(skill_id):
|
||||||
|
skill = Skill.query.get(skill_id)
|
||||||
|
if not skill:
|
||||||
|
raise ValueError("技能不存在")
|
||||||
|
|
||||||
|
if skill.is_installed:
|
||||||
|
raise ValueError("技能已安装")
|
||||||
|
|
||||||
|
skill.is_installed = True
|
||||||
|
skill.save()
|
||||||
|
return skill
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def uninstall_skill(skill_id):
|
||||||
|
skill = Skill.query.get(skill_id)
|
||||||
|
if not skill:
|
||||||
|
raise ValueError("技能不存在")
|
||||||
|
|
||||||
|
if not skill.is_installed:
|
||||||
|
raise ValueError("技能未安装")
|
||||||
|
|
||||||
|
skill.is_installed = False
|
||||||
|
skill.save()
|
||||||
|
return skill
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from ..models import Tool, AgentTool
|
||||||
|
|
||||||
|
|
||||||
|
class ToolService:
|
||||||
|
@staticmethod
|
||||||
|
def create_tool(data):
|
||||||
|
if Tool.query.filter_by(name=data["name"]).first():
|
||||||
|
raise ValueError("工具名称已存在")
|
||||||
|
|
||||||
|
tool = Tool(
|
||||||
|
name=data["name"],
|
||||||
|
type=data["type"],
|
||||||
|
description=data.get("description"),
|
||||||
|
config=data.get("config"),
|
||||||
|
)
|
||||||
|
tool.save()
|
||||||
|
return tool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_tool_to_agent(agent_id, tool_id):
|
||||||
|
if AgentTool.query.filter_by(agent_id=agent_id, tool_id=tool_id).first():
|
||||||
|
raise ValueError("工具已添加到此 Agent")
|
||||||
|
|
||||||
|
agent_tool = AgentTool(agent_id=agent_id, tool_id=tool_id)
|
||||||
|
agent_tool.save()
|
||||||
|
return agent_tool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_tool_from_agent(agent_id, tool_id):
|
||||||
|
agent_tool = AgentTool.query.filter_by(
|
||||||
|
agent_id=agent_id, tool_id=tool_id
|
||||||
|
).first()
|
||||||
|
if not agent_tool:
|
||||||
|
raise ValueError("工具关联不存在")
|
||||||
|
agent_tool.delete()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from ..models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
@staticmethod
|
||||||
|
def update_user(user, data):
|
||||||
|
if "email" in data:
|
||||||
|
if (
|
||||||
|
data["email"]
|
||||||
|
and User.query.filter(
|
||||||
|
User.email == data["email"], User.id != user.id
|
||||||
|
).first()
|
||||||
|
):
|
||||||
|
raise ValueError("邮箱已被使用")
|
||||||
|
user.email = data["email"]
|
||||||
|
|
||||||
|
if "avatar" in data:
|
||||||
|
user.avatar = data["avatar"]
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from ..models import Workspace
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceService:
|
||||||
|
@staticmethod
|
||||||
|
def create_workspace(user_id, name, description=None):
|
||||||
|
workspace = Workspace(user_id=user_id, name=name, description=description)
|
||||||
|
workspace.save()
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_workspace(workspace, data):
|
||||||
|
if "name" in data:
|
||||||
|
workspace.name = data["name"]
|
||||||
|
|
||||||
|
if "description" in data:
|
||||||
|
workspace.description = data["description"]
|
||||||
|
|
||||||
|
workspace.save()
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_workspace(workspace):
|
||||||
|
if workspace.is_default:
|
||||||
|
raise ValueError("默认工作空间不能删除")
|
||||||
|
workspace.delete()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .manager import SkillManager
|
||||||
|
from .clawhub import ClawHubMarket
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["SkillManager", "ClawHubMarket"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class ClawHubMarket:
|
||||||
|
def search_skills(self, query):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def download_skill(self, skill_id):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
class SkillManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.skills = {}
|
||||||
|
|
||||||
|
def install_skill(self, skill_path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def uninstall_skill(self, skill_name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_skills(self):
|
||||||
|
return self.skills
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .manager import ToolManager
|
||||||
|
from .mcp_adapter import MCPAdapter
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ToolManager", "MCPAdapter"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class ToolManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.tools = {}
|
||||||
|
|
||||||
|
def register_tool(self, name, tool_func):
|
||||||
|
self.tools[name] = tool_func
|
||||||
|
|
||||||
|
def execute_tool(self, name, **kwargs):
|
||||||
|
if name not in self.tools:
|
||||||
|
raise ValueError(f"Tool {name} not found")
|
||||||
|
return self.tools[name](**kwargs)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class MCPAdapter:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call_tool(self, tool_name, params):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .manager import WorkspaceManager
|
||||||
|
from .file_manager import FileManager
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["WorkspaceManager", "FileManager"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class FileManager:
|
||||||
|
def __init__(self, workspace_id, base_dir="workspaces"):
|
||||||
|
self.workspace_id = workspace_id
|
||||||
|
self.base_dir = base_dir
|
||||||
|
self.workspace_path = os.path.join(base_dir, str(workspace_id))
|
||||||
|
self._ensure_workspace_dir()
|
||||||
|
|
||||||
|
def _ensure_workspace_dir(self):
|
||||||
|
if not os.path.exists(self.workspace_path):
|
||||||
|
os.makedirs(self.workspace_path)
|
||||||
|
|
||||||
|
def save_file(self, filename, content):
|
||||||
|
file_path = os.path.join(self.workspace_path, filename)
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def read_file(self, filename):
|
||||||
|
file_path = os.path.join(self.workspace_path, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"文件 {filename} 不存在")
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def delete_file(self, filename):
|
||||||
|
file_path = os.path.join(self.workspace_path, filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
def list_files(self, directory=""):
|
||||||
|
dir_path = (
|
||||||
|
os.path.join(self.workspace_path, directory)
|
||||||
|
if directory
|
||||||
|
else self.workspace_path
|
||||||
|
)
|
||||||
|
if not os.path.exists(dir_path):
|
||||||
|
return []
|
||||||
|
files = []
|
||||||
|
for item in os.listdir(dir_path):
|
||||||
|
item_path = os.path.join(dir_path, item)
|
||||||
|
if os.path.isfile(item_path):
|
||||||
|
files.append(item)
|
||||||
|
elif os.path.isdir(item_path):
|
||||||
|
files.extend(
|
||||||
|
[
|
||||||
|
os.path.join(directory, f)
|
||||||
|
for f in self.list_files(os.path.join(directory, item))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return files
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
class WorkspaceManager:
|
||||||
|
def __init__(self, workspace_id):
|
||||||
|
self.workspace_id = workspace_id
|
||||||
|
|
||||||
|
def create_file(self, filename, content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_file(self, filename, content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_files(self):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ['py39']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
extend-exclude = '''
|
||||||
|
/(
|
||||||
|
# 默认排除
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
line_length = 100
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.9"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = false
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Flask 核心
|
||||||
|
Flask==3.0.0
|
||||||
|
Flask-CORS==4.0.0
|
||||||
|
Flask-Migrate==4.0.7
|
||||||
|
Flask-JWT-Extended==4.6.0
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
SQLAlchemy==2.0.23
|
||||||
|
PyMySQL==1.1.0
|
||||||
|
cryptography==41.0.7
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis==5.0.1
|
||||||
|
|
||||||
|
# 数据验证和序列化
|
||||||
|
marshmallow==3.20.1
|
||||||
|
flask-marshmallow==0.15.0
|
||||||
|
marshmallow-sqlalchemy==0.29.0
|
||||||
|
|
||||||
|
# HTTP 客户端
|
||||||
|
requests==2.31.0
|
||||||
|
httpx==0.25.2
|
||||||
|
|
||||||
|
# LLM 集成
|
||||||
|
openai==1.3.7
|
||||||
|
dashscope==1.14.1
|
||||||
|
anthropic==0.7.8
|
||||||
|
|
||||||
|
# 工具
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pyyaml==6.0.1
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
|
||||||
|
# 任务调度
|
||||||
|
APScheduler==3.10.4
|
||||||
|
|
||||||
|
# MCP 协议
|
||||||
|
mcp==0.1.0
|
||||||
|
|
||||||
|
# 密码加密
|
||||||
|
bcrypt==4.1.2
|
||||||
|
|
||||||
|
# JSON 处理
|
||||||
|
orjson==3.9.12
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
colorlog==6.8.0
|
||||||
|
|
||||||
|
# 工具库
|
||||||
|
tenacity==8.2.3
|
||||||
|
|
||||||
|
# 开发工具
|
||||||
|
black==24.1.1
|
||||||
|
flake8==7.0.0
|
||||||
|
mypy==1.8.0
|
||||||
|
pytest==7.4.4
|
||||||
|
pytest-cov==4.1.0
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from app import create_app
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = create_app(os.environ.get("FLASK_ENV", "development"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return {"message": "SentClaw API", "version": "0.1.0", "status": "running"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
host=app.config["Flask_HOST"],
|
||||||
|
port=app.config["Flask_PORT"],
|
||||||
|
debug=app.config["Flask_DEBUG"],
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
out
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src/renderer/src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
port: 5174
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "sentclaw-desktop",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "SentClaw Desktop App",
|
||||||
|
"main": "./out/main/index.js",
|
||||||
|
"author": "SentClaw",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"preview": "electron-vite preview",
|
||||||
|
"pack": "electron-builder --dir",
|
||||||
|
"dist": "electron-builder"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron-store": "^8.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^28.0.0",
|
||||||
|
"electron-builder": "^24.9.1",
|
||||||
|
"electron-vite": "^2.0.0",
|
||||||
|
"vite-plugin-electron": "^0.28.0",
|
||||||
|
"vite-plugin-electron-renderer": "^0.14.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { app, BrowserWindow, shell } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
function createWindow(): void {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
|
show: false,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
|
||||||
|
mainWindow.webContents.openDevTools()
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow?.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
sendMessage: (message: string) => ipcRenderer.send('message', message),
|
||||||
|
onMessage: (callback: (message: string) => void) => {
|
||||||
|
ipcRenderer.on('message', (_event, message) => callback(message))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronAPI: {
|
||||||
|
sendMessage: (message: string) => void
|
||||||
|
onMessage: (callback: (message: string) => void) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>SentClaw</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</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