first commit

This commit is contained in:
2026-04-07 16:05:05 +08:00
commit 9d9bdbb1ce
136 changed files with 5103 additions and 0 deletions
+43
View File
@@ -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
View File
@@ -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
+179
View File
@@ -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`
+305
View File
@@ -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) 的设计和实现。
+11
View File
@@ -0,0 +1,11 @@
[flake8]
max-line-length = 100
extend-ignore = E203, W503
exclude =
.git,
__pycache__,
.venv,
venv,
build,
dist,
*.egg-info
+18
View File
@@ -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"]
+43
View File
@@ -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
+63
View File
@@ -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()
+24
View File
@@ -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"}
+62
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
from .engine import AgentEngine
from .react import ReActAgent
from .plan_execute import PlanAndExecuteAgent
__all__ = ["AgentEngine", "ReActAgent", "PlanAndExecuteAgent"]
+6
View File
@@ -0,0 +1,6 @@
class AgentEngine:
def __init__(self, config):
self.config = config
def run(self, prompt, tools=None):
raise NotImplementedError
+6
View File
@@ -0,0 +1,6 @@
from .engine import AgentEngine
class PlanAndExecuteAgent(AgentEngine):
def run(self, prompt, tools=None):
pass
+6
View File
@@ -0,0 +1,6 @@
from .engine import AgentEngine
class ReActAgent(AgentEngine):
def run(self, prompt, tools=None):
pass
+18
View File
@@ -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,
)
+107
View File
@@ -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
+55
View File
@@ -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
+71
View File
@@ -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
+92
View File
@@ -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
+71
View File
@@ -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
+81
View File
@@ -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
+58
View File
@@ -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
+75
View File
@@ -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))
+45
View File
@@ -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
+80
View File
@@ -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
+40
View File
@@ -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
+82
View File
@@ -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
+14
View File
@@ -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",
]
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+69
View File
@@ -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,
}
+14
View File
@@ -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",
]
+9
View File
@@ -0,0 +1,9 @@
from .base import BaseLLMClient
class AnthropicClient(BaseLLMClient):
def chat(self, messages, **kwargs):
pass
def stream_chat(self, messages, **kwargs):
pass
+14
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
from .base import BaseLLMClient
class DashScopeClient(BaseLLMClient):
def chat(self, messages, **kwargs):
pass
def stream_chat(self, messages, **kwargs):
pass
+22
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
from .base import BaseLLMClient
class OpenAIClient(BaseLLMClient):
def chat(self, messages, **kwargs):
pass
def stream_chat(self, messages, **kwargs):
pass
+6
View File
@@ -0,0 +1,6 @@
from .manager import MemoryManager
from .extractor import MemoryExtractor
from .integrator import MemoryIntegrator
__all__ = ["MemoryManager", "MemoryExtractor", "MemoryIntegrator"]
+6
View File
@@ -0,0 +1,6 @@
class MemoryExtractor:
def __init__(self, llm_client):
self.llm_client = llm_client
def extract_from_conversation(self, conversation):
pass
+3
View File
@@ -0,0 +1,3 @@
class MemoryIntegrator:
def integrate_memories(self, memories):
pass
+13
View File
@@ -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
+15
View File
@@ -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
+37
View File
@@ -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"
)
+30
View File
@@ -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()
+15
View File
@@ -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="是否激活")
+34
View File
@@ -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"
)
+16
View File
@@ -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="下次运行时间")
+24
View File
@@ -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"
)
+38
View File
@@ -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
+14
View File
@@ -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='是否激活')
+17
View File
@@ -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="是否激活")
+29
View File
@@ -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"
)
+19
View File
@@ -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"
)
+25
View File
@@ -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"
)
+11
View File
@@ -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
+34
View File
@@ -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()
+23
View File
@@ -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()
+23
View File
@@ -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()
+27
View File
@@ -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()
+26
View File
@@ -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()
+20
View File
@@ -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)
+24
View File
@@ -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()
+15
View File
@@ -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)
+30
View File
@@ -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)
+23
View File
@@ -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)
+21
View File
@@ -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()
+12
View File
@@ -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
+47
View File
@@ -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()
+34
View File
@@ -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
+35
View File
@@ -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()
+24
View File
@@ -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
+39
View File
@@ -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()
+34
View File
@@ -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()
+15
View File
@@ -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
+51
View File
@@ -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()
+29
View File
@@ -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
+35
View File
@@ -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()
+21
View File
@@ -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
+26
View File
@@ -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()
+5
View File
@@ -0,0 +1,5 @@
from .manager import SkillManager
from .clawhub import ClawHubMarket
__all__ = ["SkillManager", "ClawHubMarket"]
+6
View File
@@ -0,0 +1,6 @@
class ClawHubMarket:
def search_skills(self, query):
pass
def download_skill(self, skill_id):
pass
+12
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
from .manager import ToolManager
from .mcp_adapter import MCPAdapter
__all__ = ["ToolManager", "MCPAdapter"]
+11
View File
@@ -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)
+9
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
from .manager import WorkspaceManager
from .file_manager import FileManager
__all__ = ["WorkspaceManager", "FileManager"]
+53
View File
@@ -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
+18
View File
@@ -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
+35
View File
@@ -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_*"]
+57
View File
@@ -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
+22
View File
@@ -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"],
)
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
dist-ssr
*.local
out
.DS_Store
+23
View File
@@ -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
}
}
})
+25
View File
@@ -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"
}
}
+56
View File
@@ -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()
}
})
})
+17
View File
@@ -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
}
}
}
+12
View File
@@ -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