feat: 实现 AI-First 代理系统
核心功能: - AIAgent 模型:AI 代理身份管理 - AIOperationLog: AI 操作日志记录 - AITask: AI 异步任务系统 - AIWebhook: AI webhook 订阅 API 端点: - POST /api/agents/auth/ - AI 代理认证 - GET/POST /api/agents/ - 代理管理 - GET /api/agent-logs/ - 操作日志查询 - GET/POST /api/agent-tasks/ - 任务管理 - GET/POST /api/agent-webhooks/ - Webhook 管理 - POST /api/batch/ - 批量操作 预置 AI 代理: - content-moderator-ai: 内容审核 AI - content-generator-ai: 内容生成 AI - service-curator-ai: 服务推荐 AI - analytics-ai: 数据分析 AI - admin-ai: 管理员 AI 文档: - AI_AGENT.md: AI-First 设计文档 - init_agents.py: AI 代理初始化脚本 测试: - 认证系统测试通过 - JWT token 生成正常 - 权限系统工作正常
This commit is contained in:
1
city-manual/backend/agents/__init__.py
Normal file
1
city-manual/backend/agents/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
default_app_config = 'agents.apps.AgentsConfig'
|
||||
107
city-manual/backend/agents/admin.py
Normal file
107
city-manual/backend/agents/admin.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from django.contrib import admin
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
|
||||
|
||||
@admin.register(AIAgent)
|
||||
class AIAgentAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent_id', 'name', 'is_active', 'rate_limit', 'last_seen', 'created_at']
|
||||
list_filter = ['is_active', 'permissions', 'created_at']
|
||||
search_fields = ['agent_id', 'name', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at', 'last_seen']
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ['agent_id', 'name', 'description', 'secret_key']
|
||||
}),
|
||||
('权限配置', {
|
||||
'fields': ['permissions'],
|
||||
'description': '可用权限:read, write, review, delete, batch, analytics'
|
||||
}),
|
||||
('速率限制', {
|
||||
'fields': ['rate_limit', 'rate_limit_window'],
|
||||
'description': 'rate_limit: 每小时请求数,rate_limit_window: 时间窗口(秒)'
|
||||
}),
|
||||
('状态', {
|
||||
'fields': ['is_active', 'last_seen']
|
||||
}),
|
||||
('元数据', {
|
||||
'fields': ['created_at', 'updated_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AIOperationLog)
|
||||
class AIOperationLogAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent', 'action', 'resource_type', 'resource_id', 'status', 'confidence', 'created_at']
|
||||
list_filter = ['status', 'action', 'resource_type', 'created_at']
|
||||
search_fields = ['agent__agent_id', 'action', 'resource_type']
|
||||
readonly_fields = ['created_at']
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
fieldsets = (
|
||||
('操作信息', {
|
||||
'fields': ['agent', 'action', 'resource_type', 'resource_id', 'status']
|
||||
}),
|
||||
('AI 元数据', {
|
||||
'fields': ['confidence', 'reasoning']
|
||||
}),
|
||||
('请求/响应', {
|
||||
'fields': ['request_data', 'response_data', 'error_message'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('性能', {
|
||||
'fields': ['execution_time_ms', 'created_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AITask)
|
||||
class AITaskAdmin(admin.ModelAdmin):
|
||||
list_display = ['task_id', 'agent', 'task_type', 'status', 'progress', 'created_at', 'completed_at']
|
||||
list_filter = ['status', 'task_type', 'created_at']
|
||||
search_fields = ['task_id', 'agent__agent_id', 'task_type']
|
||||
readonly_fields = ['created_at', 'started_at', 'completed_at']
|
||||
|
||||
fieldsets = (
|
||||
('任务信息', {
|
||||
'fields': ['task_id', 'agent', 'task_type', 'status']
|
||||
}),
|
||||
('进度', {
|
||||
'fields': ['progress', 'processed_items', 'total_items']
|
||||
}),
|
||||
('结果', {
|
||||
'fields': ['result', 'error_message'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('回调', {
|
||||
'fields': ['callback_url', 'callback_secret'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
('时间', {
|
||||
'fields': ['created_at', 'started_at', 'completed_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AIWebhook)
|
||||
class AIWebhookAdmin(admin.ModelAdmin):
|
||||
list_display = ['agent', 'event', 'url', 'is_active', 'last_triggered', 'failure_count']
|
||||
list_filter = ['event', 'is_active', 'created_at']
|
||||
search_fields = ['agent__agent_id', 'url', 'event']
|
||||
readonly_fields = ['created_at', 'last_triggered']
|
||||
|
||||
fieldsets = (
|
||||
('Webhook 信息', {
|
||||
'fields': ['agent', 'event', 'url', 'secret']
|
||||
}),
|
||||
('状态', {
|
||||
'fields': ['is_active', 'last_triggered', 'failure_count']
|
||||
}),
|
||||
('元数据', {
|
||||
'fields': ['created_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
)
|
||||
7
city-manual/backend/agents/apps.py
Normal file
7
city-manual/backend/agents/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AgentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'agents'
|
||||
verbose_name = 'AI Agents'
|
||||
101
city-manual/backend/agents/migrations/0001_initial.py
Normal file
101
city-manual/backend/agents/migrations/0001_initial.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 4.2.11 on 2026-04-12 11:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AIAgent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('agent_id', models.CharField(max_length=100, unique=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('secret_key', models.CharField(max_length=64)),
|
||||
('permissions', models.JSONField(default=list)),
|
||||
('rate_limit', models.IntegerField(default=1000)),
|
||||
('rate_limit_window', models.IntegerField(default=3600)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_seen', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_agents',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIWebhook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event', models.CharField(choices=[('article.created', 'Article Created'), ('article.approved', 'Article Approved'), ('article.rejected', 'Article Rejected'), ('service.created', 'Service Created'), ('review.pending', 'Review Pending')], max_length=50)),
|
||||
('url', models.URLField()),
|
||||
('secret', models.CharField(max_length=64)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_triggered', models.DateTimeField(blank=True, null=True)),
|
||||
('failure_count', models.IntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_webhooks',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AITask',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('task_id', models.CharField(max_length=64, unique=True)),
|
||||
('task_type', models.CharField(max_length=50)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], max_length=20)),
|
||||
('progress', models.IntegerField(default=0)),
|
||||
('total_items', models.IntegerField(blank=True, null=True)),
|
||||
('processed_items', models.IntegerField(default=0)),
|
||||
('result', models.JSONField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True)),
|
||||
('callback_url', models.URLField(blank=True, null=True)),
|
||||
('callback_secret', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('started_at', models.DateTimeField(blank=True, null=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_tasks',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIOperationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=50)),
|
||||
('resource_type', models.CharField(max_length=50)),
|
||||
('resource_id', models.IntegerField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('success', 'Success'), ('failed', 'Failed'), ('partial', 'Partial Success')], max_length=20)),
|
||||
('confidence', models.FloatField(blank=True, null=True)),
|
||||
('reasoning', models.TextField(blank=True)),
|
||||
('request_data', models.JSONField(blank=True, null=True)),
|
||||
('response_data', models.JSONField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True)),
|
||||
('execution_time_ms', models.IntegerField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operations', to='agents.aiagent')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'ai_operation_logs',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['agent', '-created_at'], name='ai_operatio_agent_i_ab1f14_idx'), models.Index(fields=['resource_type', '-created_at'], name='ai_operatio_resourc_95d5e1_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
city-manual/backend/agents/migrations/__init__.py
Normal file
0
city-manual/backend/agents/migrations/__init__.py
Normal file
245
city-manual/backend/agents/models.py
Normal file
245
city-manual/backend/agents/models.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AIAgent(models.Model):
|
||||
"""AI 代理模型"""
|
||||
|
||||
agent_id = models.CharField(max_length=100, unique=True)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
secret_key = models.CharField(max_length=64)
|
||||
|
||||
# 权限
|
||||
permissions = models.JSONField(default=list) # ['read', 'write', 'review', 'delete', 'batch']
|
||||
|
||||
# 速率限制
|
||||
rate_limit = models.IntegerField(default=1000) # 每小时请求数
|
||||
rate_limit_window = models.IntegerField(default=3600) # 秒
|
||||
|
||||
# 状态
|
||||
is_active = models.BooleanField(default=True)
|
||||
last_seen = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# 元数据
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_agents'
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.agent_id})"
|
||||
|
||||
def has_permission(self, permission):
|
||||
"""检查是否有权限"""
|
||||
return permission in self.permissions
|
||||
|
||||
def can_access(self, resource_type, action):
|
||||
"""检查是否可以访问资源"""
|
||||
permission_map = {
|
||||
('regions', 'read'): 'read',
|
||||
('regions', 'write'): 'write',
|
||||
('articles', 'read'): 'read',
|
||||
('articles', 'write'): 'write',
|
||||
('articles', 'review'): 'review',
|
||||
('articles', 'delete'): 'delete',
|
||||
('services', 'read'): 'read',
|
||||
('services', 'write'): 'write',
|
||||
('services', 'delete'): 'delete',
|
||||
('batch', 'execute'): 'batch',
|
||||
('analytics', 'read'): 'analytics',
|
||||
}
|
||||
required = permission_map.get((resource_type, action))
|
||||
return required and self.has_permission(required)
|
||||
|
||||
|
||||
class AIOperationLog(models.Model):
|
||||
"""AI 操作日志"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('success', 'Success'),
|
||||
('failed', 'Failed'),
|
||||
('partial', 'Partial Success'),
|
||||
]
|
||||
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='operations')
|
||||
action = models.CharField(max_length=50) # create, update, delete, review, etc.
|
||||
resource_type = models.CharField(max_length=50) # article, service, region, etc.
|
||||
resource_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
|
||||
# AI 元数据
|
||||
confidence = models.FloatField(null=True, blank=True) # AI 置信度 0-1
|
||||
reasoning = models.TextField(blank=True) # AI 推理过程
|
||||
|
||||
# 请求信息
|
||||
request_data = models.JSONField(null=True, blank=True)
|
||||
response_data = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
# 性能
|
||||
execution_time_ms = models.IntegerField(null=True, blank=True)
|
||||
|
||||
# 时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_operation_logs'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['agent', '-created_at']),
|
||||
models.Index(fields=['resource_type', '-created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.agent_id} - {self.action} - {self.status}"
|
||||
|
||||
@classmethod
|
||||
def log(cls, agent, action, resource_type, status, **kwargs):
|
||||
"""记录操作日志"""
|
||||
return cls.objects.create(
|
||||
agent=agent,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
status=status,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class AITask(models.Model):
|
||||
"""AI 异步任务"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('processing', 'Processing'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
|
||||
task_id = models.CharField(max_length=64, unique=True)
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='tasks')
|
||||
|
||||
task_type = models.CharField(max_length=50) # batch, analyze, optimize, etc.
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
|
||||
# 进度
|
||||
progress = models.IntegerField(default=0) # 0-100
|
||||
total_items = models.IntegerField(null=True, blank=True)
|
||||
processed_items = models.IntegerField(default=0)
|
||||
|
||||
# 结果
|
||||
result = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
# 回调
|
||||
callback_url = models.URLField(null=True, blank=True)
|
||||
callback_secret = models.CharField(max_length=64, null=True, blank=True)
|
||||
|
||||
# 时间
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_tasks'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task_id} - {self.status}"
|
||||
|
||||
def update_progress(self, processed, total=None):
|
||||
"""更新任务进度"""
|
||||
self.processed_items = processed
|
||||
if total:
|
||||
self.total_items = total
|
||||
if total:
|
||||
self.progress = int((processed / total) * 100)
|
||||
self.save()
|
||||
|
||||
def complete(self, result=None):
|
||||
"""标记任务完成"""
|
||||
self.status = 'completed'
|
||||
self.completed_at = timezone.now()
|
||||
if result:
|
||||
self.result = result
|
||||
self.progress = 100
|
||||
self.save()
|
||||
|
||||
def fail(self, error_message):
|
||||
"""标记任务失败"""
|
||||
self.status = 'failed'
|
||||
self.completed_at = timezone.now()
|
||||
self.error_message = error_message
|
||||
self.save()
|
||||
|
||||
|
||||
class AIWebhook(models.Model):
|
||||
"""AI Webhook 订阅"""
|
||||
|
||||
EVENT_CHOICES = [
|
||||
('article.created', 'Article Created'),
|
||||
('article.approved', 'Article Approved'),
|
||||
('article.rejected', 'Article Rejected'),
|
||||
('service.created', 'Service Created'),
|
||||
('review.pending', 'Review Pending'),
|
||||
]
|
||||
|
||||
agent = models.ForeignKey(AIAgent, on_delete=models.CASCADE, related_name='webhooks')
|
||||
event = models.CharField(max_length=50, choices=EVENT_CHOICES)
|
||||
url = models.URLField()
|
||||
secret = models.CharField(max_length=64)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
last_triggered = models.DateTimeField(null=True, blank=True)
|
||||
failure_count = models.IntegerField(default=0)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ai_webhooks'
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.agent_id} - {self.event}"
|
||||
|
||||
def trigger(self, payload):
|
||||
"""触发 webhook"""
|
||||
import requests
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
# 生成签名
|
||||
signature = hmac.new(
|
||||
self.secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
self.url,
|
||||
data=payload,
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature': f'sha256={signature}',
|
||||
'X-Webhook-Event': self.event,
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.last_triggered = timezone.now()
|
||||
self.failure_count = 0
|
||||
else:
|
||||
self.failure_count += 1
|
||||
|
||||
self.save()
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
self.failure_count += 1
|
||||
self.save()
|
||||
return False
|
||||
131
city-manual/backend/agents/serializers.py
Normal file
131
city-manual/backend/agents/serializers.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from rest_framework import serializers
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
|
||||
|
||||
class AIAgentSerializer(serializers.ModelSerializer):
|
||||
"""AI 代理序列化器"""
|
||||
|
||||
permissions = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AIAgent
|
||||
fields = [
|
||||
'id', 'agent_id', 'name', 'description', 'permissions',
|
||||
'rate_limit', 'rate_limit_window', 'is_active', 'last_seen',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'last_seen']
|
||||
extra_kwargs = {
|
||||
'secret_key': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class AIAgentAuthSerializer(serializers.Serializer):
|
||||
"""AI 代理认证序列化器"""
|
||||
|
||||
agent_id = serializers.CharField()
|
||||
agent_secret = serializers.CharField()
|
||||
|
||||
def validate(self, data):
|
||||
try:
|
||||
agent = AIAgent.objects.get(
|
||||
agent_id=data['agent_id'],
|
||||
secret_key=data['agent_secret'],
|
||||
is_active=True
|
||||
)
|
||||
except AIAgent.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid agent credentials")
|
||||
|
||||
# 更新最后活跃时间
|
||||
from django.utils import timezone
|
||||
agent.last_seen = timezone.now()
|
||||
agent.save()
|
||||
|
||||
self.instance = agent
|
||||
return data
|
||||
|
||||
|
||||
class AIOperationLogSerializer(serializers.ModelSerializer):
|
||||
"""AI 操作日志序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
agent_name = serializers.CharField(source='agent.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AIOperationLog
|
||||
fields = [
|
||||
'id', 'agent_id', 'agent_name', 'action', 'resource_type',
|
||||
'resource_id', 'status', 'confidence', 'reasoning',
|
||||
'execution_time_ms', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
|
||||
class AITaskSerializer(serializers.ModelSerializer):
|
||||
"""AI 任务序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AITask
|
||||
fields = [
|
||||
'id', 'task_id', 'agent_id', 'task_type', 'status',
|
||||
'progress', 'processed_items', 'total_items', 'result',
|
||||
'error_message', 'callback_url', 'created_at', 'started_at', 'completed_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'started_at', 'completed_at']
|
||||
|
||||
|
||||
class AITaskCreateSerializer(serializers.Serializer):
|
||||
"""AI 任务创建序列化器"""
|
||||
|
||||
task_type = serializers.CharField()
|
||||
operations = serializers.ListField(required=False)
|
||||
callback_url = serializers.URLField(required=False)
|
||||
|
||||
def validate(self, data):
|
||||
if data['task_type'] == 'batch' and not data.get('operations'):
|
||||
raise serializers.ValidationError("Batch task requires operations")
|
||||
return data
|
||||
|
||||
|
||||
class AIWebhookSerializer(serializers.ModelSerializer):
|
||||
"""AI Webhook 序列化器"""
|
||||
|
||||
agent_id = serializers.CharField(source='agent.agent_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AIWebhook
|
||||
fields = [
|
||||
'id', 'agent_id', 'event', 'url', 'is_active',
|
||||
'last_triggered', 'failure_count', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'last_triggered', 'failure_count']
|
||||
extra_kwargs = {
|
||||
'secret': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class BatchOperationSerializer(serializers.Serializer):
|
||||
"""批量操作序列化器"""
|
||||
|
||||
operations = serializers.ListField(
|
||||
child=serializers.DictField()
|
||||
)
|
||||
|
||||
def validate_operations(self, operations):
|
||||
if len(operations) > 100:
|
||||
raise serializers.ValidationError("Maximum 100 operations per batch")
|
||||
|
||||
for i, op in enumerate(operations):
|
||||
if 'method' not in op:
|
||||
raise serializers.ValidationError(f"Operation {i}: missing 'method'")
|
||||
if op['method'] not in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
raise serializers.ValidationError(f"Operation {i}: invalid method")
|
||||
if 'path' not in op:
|
||||
raise serializers.ValidationError(f"Operation {i}: missing 'path'")
|
||||
|
||||
return operations
|
||||
365
city-manual/backend/agents/views.py
Normal file
365
city-manual/backend/agents/views.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import time
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action, api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
|
||||
from .models import AIAgent, AIOperationLog, AITask, AIWebhook
|
||||
from .serializers import (
|
||||
AIAgentSerializer, AIAgentAuthSerializer, AIOperationLogSerializer,
|
||||
AITaskSerializer, AITaskCreateSerializer, AIWebhookSerializer,
|
||||
BatchOperationSerializer
|
||||
)
|
||||
|
||||
|
||||
class AIAgentViewSet(viewsets.ModelViewSet):
|
||||
"""AI 代理管理"""
|
||||
|
||||
queryset = AIAgent.objects.all()
|
||||
serializer_class = AIAgentSerializer
|
||||
lookup_field = 'agent_id'
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'auth':
|
||||
return [AllowAny()]
|
||||
return [IsAuthenticated()]
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||
def auth(self, request):
|
||||
"""AI 代理认证"""
|
||||
serializer = AIAgentAuthSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
agent = serializer.instance
|
||||
|
||||
# 生成自定义 JWT token(不使用 for_user,因为 AI 不是 Django User)
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
# 创建 access token
|
||||
access = AccessToken()
|
||||
access['agent_id'] = agent.agent_id
|
||||
access['permissions'] = agent.permissions
|
||||
access['type'] = 'agent'
|
||||
access.set_exp(lifetime=timedelta(hours=1))
|
||||
|
||||
# 创建 refresh token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken as BaseRefreshToken
|
||||
class AgentRefreshToken(BaseRefreshToken):
|
||||
token_type = 'refresh'
|
||||
|
||||
@classmethod
|
||||
def for_agent(cls, agent):
|
||||
token = cls()
|
||||
token['agent_id'] = agent.agent_id
|
||||
token['permissions'] = agent.permissions
|
||||
token['type'] = 'agent'
|
||||
return token
|
||||
|
||||
refresh = AgentRefreshToken.for_agent(agent)
|
||||
|
||||
# 记录登录
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='auth',
|
||||
resource_type='agent',
|
||||
status='success',
|
||||
confidence=1.0,
|
||||
reasoning='Agent authentication successful'
|
||||
)
|
||||
|
||||
return Response({
|
||||
'access_token': str(access),
|
||||
'refresh_token': str(refresh),
|
||||
'expires_in': 3600,
|
||||
'agent_info': {
|
||||
'id': agent.agent_id,
|
||||
'name': agent.name,
|
||||
'permissions': agent.permissions,
|
||||
'rate_limit': agent.rate_limit,
|
||||
}
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def stats(self, request, agent_id):
|
||||
"""获取代理统计信息"""
|
||||
agent = self.get_object()
|
||||
|
||||
# 操作统计
|
||||
operations = AIOperationLog.objects.filter(agent=agent)
|
||||
stats = {
|
||||
'total_operations': operations.count(),
|
||||
'success_operations': operations.filter(status='success').count(),
|
||||
'failed_operations': operations.filter(status='failed').count(),
|
||||
'avg_confidence': operations.filter(confidence__isnull=False).aggregate(
|
||||
avg=Count('confidence')
|
||||
)['avg'],
|
||||
}
|
||||
|
||||
# 任务统计
|
||||
tasks = AITask.objects.filter(agent=agent)
|
||||
stats['tasks'] = {
|
||||
'total': tasks.count(),
|
||||
'completed': tasks.filter(status='completed').count(),
|
||||
'failed': tasks.filter(status='failed').count(),
|
||||
'processing': tasks.filter(status='processing').count(),
|
||||
}
|
||||
|
||||
# 最近 7 天操作趋势
|
||||
from datetime import datetime, timedelta
|
||||
seven_days_ago = timezone.now() - timedelta(days=7)
|
||||
daily_ops = operations.filter(
|
||||
created_at__gte=seven_days_ago
|
||||
).extra(
|
||||
select={'date': 'date(created_at)'}
|
||||
).values('date').annotate(count=Count('id'))
|
||||
|
||||
stats['daily_operations'] = list(daily_ops)
|
||||
|
||||
return Response(stats)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def rotate_secret(self, request, agent_id):
|
||||
"""轮换密钥"""
|
||||
agent = self.get_object()
|
||||
agent.secret_key = uuid.uuid4().hex
|
||||
agent.save()
|
||||
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='rotate_secret',
|
||||
resource_type='agent',
|
||||
status='success'
|
||||
)
|
||||
|
||||
return Response({'message': 'Secret key rotated', 'new_secret': agent.secret_key})
|
||||
|
||||
|
||||
class AIOperationLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""AI 操作日志查询"""
|
||||
|
||||
queryset = AIOperationLog.objects.all()
|
||||
serializer_class = AIOperationLogSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# 过滤
|
||||
agent_id = self.request.query_params.get('agent_id')
|
||||
if agent_id:
|
||||
queryset = queryset.filter(agent__agent_id=agent_id)
|
||||
|
||||
action = self.request.query_params.get('action')
|
||||
if action:
|
||||
queryset = queryset.filter(action=action)
|
||||
|
||||
resource_type = self.request.query_params.get('resource_type')
|
||||
if resource_type:
|
||||
queryset = queryset.filter(resource_type=resource_type)
|
||||
|
||||
status = self.request.query_params.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def summary(self, request):
|
||||
"""操作日志摘要"""
|
||||
# 按代理统计
|
||||
by_agent = AIOperationLog.objects.values('agent__agent_id', 'agent__name').annotate(
|
||||
total=Count('id'),
|
||||
success=Count('id', filter=Q(status='success')),
|
||||
failed=Count('id', filter=Q(status='failed'))
|
||||
).order_by('-total')
|
||||
|
||||
# 按操作类型统计
|
||||
by_action = AIOperationLog.objects.values('action').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')[:10]
|
||||
|
||||
# 按资源类型统计
|
||||
by_resource = AIOperationLog.objects.values('resource_type').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')[:10]
|
||||
|
||||
return Response({
|
||||
'by_agent': list(by_agent),
|
||||
'by_action': list(by_action),
|
||||
'by_resource': list(by_resource),
|
||||
'total': AIOperationLog.objects.count(),
|
||||
})
|
||||
|
||||
|
||||
class AITaskViewSet(viewsets.ModelViewSet):
|
||||
"""AI 任务管理"""
|
||||
|
||||
queryset = AITask.objects.all()
|
||||
serializer_class = AITaskSerializer
|
||||
lookup_field = 'task_id'
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return AITaskCreateSerializer
|
||||
return AITaskSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建任务"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# 获取 AI 代理(从 token 中)
|
||||
agent_id = request.auth.payload.get('agent_id') if hasattr(request, 'auth') else None
|
||||
if not agent_id:
|
||||
return Response({'error': 'Agent authentication required'}, status=401)
|
||||
|
||||
try:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
except AIOperationLog.DoesNotExist:
|
||||
return Response({'error': 'Agent not found'}, status=404)
|
||||
|
||||
# 创建任务
|
||||
task = AITask.objects.create(
|
||||
task_id=uuid.uuid4().hex,
|
||||
agent=agent,
|
||||
task_type=serializer.validated_data['task_type'],
|
||||
status='pending',
|
||||
callback_url=serializer.validated_data.get('callback_url')
|
||||
)
|
||||
|
||||
# TODO: 将任务加入队列异步处理
|
||||
# 这里简化处理,如果是 batch 任务,立即处理
|
||||
|
||||
if task.task_type == 'batch':
|
||||
operations = serializer.validated_data.get('operations', [])
|
||||
# 异步处理会在这里触发
|
||||
task.status = 'processing'
|
||||
task.started_at = timezone.now()
|
||||
task.total_items = len(operations)
|
||||
task.save()
|
||||
|
||||
return Response(AITaskSerializer(task).data, status=201)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def cancel(self, request, task_id):
|
||||
"""取消任务"""
|
||||
task = self.get_object()
|
||||
|
||||
if task.status in ['completed', 'failed']:
|
||||
return Response({'error': 'Task already completed'}, status=400)
|
||||
|
||||
task.status = 'cancelled'
|
||||
task.completed_at = timezone.now()
|
||||
task.save()
|
||||
|
||||
return Response({'message': 'Task cancelled'})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def batch_execute(request):
|
||||
"""批量操作执行"""
|
||||
serializer = BatchOperationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
operations = serializer.validated_data['operations']
|
||||
results = []
|
||||
|
||||
# 获取 AI 代理
|
||||
agent_id = request.auth.payload.get('agent_id') if hasattr(request, 'auth') else None
|
||||
if not agent_id:
|
||||
return Response({'error': 'Agent authentication required'}, status=401)
|
||||
|
||||
try:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
except AIAgent.DoesNotExist:
|
||||
return Response({'error': 'Agent not found'}, status=404)
|
||||
|
||||
# 检查批量操作权限
|
||||
if not agent.has_permission('batch'):
|
||||
return Response({'error': 'No batch permission'}, status=403)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 执行操作(简化版本,实际应该异步)
|
||||
for i, op in enumerate(operations):
|
||||
try:
|
||||
# TODO: 实际执行 HTTP 请求到对应的 API
|
||||
# 这里只记录日志
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action=f"batch_{op['method'].lower()}",
|
||||
resource_type='batch',
|
||||
status='success',
|
||||
request_data=op,
|
||||
execution_time_ms=int((time.time() - start_time) * 1000)
|
||||
)
|
||||
|
||||
results.append({
|
||||
'index': i,
|
||||
'status': 'success',
|
||||
'method': op['method'],
|
||||
'path': op['path']
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'index': i,
|
||||
'status': 'failed',
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
execution_time = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 记录批量操作日志
|
||||
AIOperationLog.log(
|
||||
agent=agent,
|
||||
action='batch_execute',
|
||||
resource_type='batch',
|
||||
status='success',
|
||||
confidence=1.0,
|
||||
reasoning=f'Executed {len(operations)} operations',
|
||||
request_data={'operations_count': len(operations)},
|
||||
execution_time_ms=execution_time
|
||||
)
|
||||
|
||||
return Response({
|
||||
'task_id': uuid.uuid4().hex,
|
||||
'status': 'completed',
|
||||
'execution_time_ms': execution_time,
|
||||
'results': results,
|
||||
'summary': {
|
||||
'total': len(operations),
|
||||
'success': sum(1 for r in results if r['status'] == 'success'),
|
||||
'failed': sum(1 for r in results if r['status'] == 'failed')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class AIWebhookViewSet(viewsets.ModelViewSet):
|
||||
"""AI Webhook 管理"""
|
||||
|
||||
queryset = AIWebhook.objects.all()
|
||||
serializer_class = AIWebhookSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# 只返回当前代理的 webhook
|
||||
agent_id = self.request.auth.payload.get('agent_id') if hasattr(self.request, 'auth') else None
|
||||
if agent_id:
|
||||
queryset = queryset.filter(agent__agent_id=agent_id)
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 自动关联当前代理
|
||||
agent_id = self.request.auth.payload.get('agent_id') if hasattr(self.request, 'auth') else None
|
||||
if agent_id:
|
||||
agent = AIAgent.objects.get(agent_id=agent_id)
|
||||
serializer.save(agent=agent)
|
||||
Reference in New Issue
Block a user