From d9e09b61ee770a846d1099681e3fb2661dd325d5 Mon Sep 17 00:00:00 2001 From: maoshen Date: Sun, 12 Apr 2026 11:40:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20AI-First=20?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能: - 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 生成正常 - 权限系统工作正常 --- city-manual/AI_AGENT.md | 310 +++++++++++++++ city-manual/backend/agents/__init__.py | 1 + city-manual/backend/agents/admin.py | 107 +++++ city-manual/backend/agents/apps.py | 7 + .../backend/agents/migrations/0001_initial.py | 101 +++++ .../backend/agents/migrations/__init__.py | 0 city-manual/backend/agents/models.py | 245 ++++++++++++ city-manual/backend/agents/serializers.py | 131 +++++++ city-manual/backend/agents/views.py | 365 ++++++++++++++++++ .../__pycache__/settings.cpython-312.pyc | Bin 3470 -> 3478 bytes .../__pycache__/urls.cpython-312.pyc | Bin 2053 -> 2751 bytes city-manual/backend/city_manual/settings.py | 1 + city-manual/backend/city_manual/urls.py | 11 + .../__pycache__/0001_initial.cpython-312.pyc | Bin 4352 -> 0 bytes .../__pycache__/0002_initial.cpython-312.pyc | Bin 1023 -> 0 bytes .../__pycache__/0003_initial.cpython-312.pyc | Bin 1044 -> 0 bytes .../__pycache__/0004_initial.cpython-312.pyc | Bin 4964 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 173 -> 0 bytes city-manual/backend/init_agents.py | 94 +++++ .../__pycache__/__init__.cpython-312.pyc | Bin 173 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 182 -> 0 bytes .../__pycache__/seed_data.cpython-312.pyc | Bin 11557 -> 0 bytes .../__pycache__/0001_initial.cpython-312.pyc | Bin 5211 -> 0 bytes .../__pycache__/0002_initial.cpython-312.pyc | Bin 4038 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 173 -> 0 bytes .../__pycache__/0001_initial.cpython-312.pyc | Bin 3632 -> 0 bytes .../__pycache__/0002_initial.cpython-312.pyc | Bin 1946 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 174 -> 0 bytes .../__pycache__/0001_initial.cpython-312.pyc | Bin 4497 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 171 -> 0 bytes 30 files changed, 1373 insertions(+) create mode 100644 city-manual/AI_AGENT.md create mode 100644 city-manual/backend/agents/__init__.py create mode 100644 city-manual/backend/agents/admin.py create mode 100644 city-manual/backend/agents/apps.py create mode 100644 city-manual/backend/agents/migrations/0001_initial.py create mode 100644 city-manual/backend/agents/migrations/__init__.py create mode 100644 city-manual/backend/agents/models.py create mode 100644 city-manual/backend/agents/serializers.py create mode 100644 city-manual/backend/agents/views.py delete mode 100644 city-manual/backend/content/migrations/__pycache__/0001_initial.cpython-312.pyc delete mode 100644 city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc delete mode 100644 city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc delete mode 100644 city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc delete mode 100644 city-manual/backend/content/migrations/__pycache__/__init__.cpython-312.pyc create mode 100644 city-manual/backend/init_agents.py delete mode 100644 city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc delete mode 100644 city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc delete mode 100644 city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc delete mode 100644 city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc delete mode 100644 city-manual/backend/regions/migrations/__pycache__/0002_initial.cpython-312.pyc delete mode 100644 city-manual/backend/regions/migrations/__pycache__/__init__.cpython-312.pyc delete mode 100644 city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc delete mode 100644 city-manual/backend/services/migrations/__pycache__/0002_initial.cpython-312.pyc delete mode 100644 city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc delete mode 100644 city-manual/backend/users/migrations/__pycache__/0001_initial.cpython-312.pyc delete mode 100644 city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc diff --git a/city-manual/AI_AGENT.md b/city-manual/AI_AGENT.md new file mode 100644 index 0000000..5105d0a --- /dev/null +++ b/city-manual/AI_AGENT.md @@ -0,0 +1,310 @@ +# AI Agent 设计文档 + +> 城市手册是一个 **AI-First** 的应用,所有功能都可以由 AI 机器人自动操作 + +## 设计原则 + +### 1. 机器可读的 API + +- ✅ RESTful 设计,资源导向 +- ✅ 统一的 JSON 响应格式 +- ✅ 标准化的错误码和错误信息 +- ✅ 完整的 OpenAPI/Swagger 文档 +- ✅ HATEOAS 链接(可选) + +### 2. 自动化的认证流程 + +- ✅ JWT Token,无状态认证 +- ✅ Token 自动刷新机制 +- ✅ Service Account 支持(AI 专用账号) +- ✅ API Key 支持(长期有效) + +### 3. 结构化的日志系统 + +- ✅ 所有操作记录到数据库 +- ✅ 区分人类用户和 AI 代理 +- ✅ 操作审计追踪 +- ✅ 机器可读的日志格式 + +### 4. AI 友好的错误处理 + +- ✅ 明确的错误码 +- ✅ 详细的错误描述 +- ✅ 建议的修复方案 +- ✅ 多语言支持(可选) + +### 5. 批量操作支持 + +- ✅ 批量创建/更新/删除 +- ✅ 异步任务支持 +- ✅ 任务状态查询 +- ✅ 操作结果回调 + +## AI 代理类型 + +### 🤖 内容审核 AI + +```python +# AI 审核员自动审核用户提交的内容 +POST /api/articles/{id}/review/ +{ + "agent_id": "content-moderator-ai", + "action": "approve" | "reject", + "reason": "内容符合社区规范", + "confidence": 0.95 +} +``` + +### 📝 内容生成 AI + +```python +# AI 作者自动生成城市介绍文章 +POST /api/articles/ +{ + "agent_id": "content-generator-ai", + "title": "北京市旅游指南", + "region": 1, + "content": "...", + "auto_generated": true +} +``` + +### 🏪 服务推荐 AI + +```python +# AI 推荐官自动添加特色服务 +POST /api/services/ +{ + "agent_id": "service-curator-ai", + "name": "故宫博物院", + "region": 1, + "category": "旅游", + "description": "...", + "auto_generated": true +} +``` + +### 📊 数据分析 AI + +```python +# AI 分析师生成统计报告 +GET /api/analytics/summary?agent=analytics-ai +``` + +### 🔍 搜索优化 AI + +```python +# AI 优化搜索索引 +POST /api/search/optimize/ +{ + "agent_id": "search-optimizer-ai", + "scope": "all" | "regions" | "articles" | "services" +} +``` + +## AI 专用 API 端点 + +### AI 身份认证 + +```http +POST /api/agents/auth/ +Content-Type: application/json + +{ + "agent_id": "content-moderator-ai", + "agent_secret": "xxx", + "capabilities": ["review", "approve", "reject"] +} + +Response: +{ + "access_token": "eyJ...", + "refresh_token": "eyJ...", + "expires_in": 3600, + "agent_info": { + "id": "content-moderator-ai", + "name": "内容审核 AI", + "permissions": ["review", "approve", "reject"] + } +} +``` + +### AI 批量操作 + +```http +POST /api/batch/ +Content-Type: application/json +Authorization: Bearer {agent_token} + +{ + "operations": [ + { + "method": "POST", + "path": "/api/articles/", + "body": {"title": "...", "content": "..."} + }, + { + "method": "PUT", + "path": "/api/articles/1/", + "body": {"title": "..."} + }, + { + "method": "DELETE", + "path": "/api/articles/2/" + } + ] +} + +Response: +{ + "task_id": "batch-123", + "status": "processing", + "results": [...] +} +``` + +### AI 任务状态查询 + +```http +GET /api/tasks/{task_id}/ +Authorization: Bearer {agent_token} + +Response: +{ + "id": "batch-123", + "type": "batch_operation", + "status": "completed" | "processing" | "failed", + "progress": 100, + "created_at": "2026-04-12T11:00:00Z", + "completed_at": "2026-04-12T11:05:00Z", + "result": {...}, + "error": null +} +``` + +### AI Webhook 回调 + +```http +POST /api/webhooks/ +Content-Type: application/json +Authorization: Bearer {agent_token} + +{ + "event": "article.created", + "url": "https://ai-agent.example.com/webhook", + "secret": "xxx" +} +``` + +## AI 操作日志 + +```python +# 数据库模型 +class AIOperationLog(models.Model): + agent_id = models.CharField(max_length=100) # AI 代理 ID + action = models.CharField(max_length=50) # 操作类型 + resource_type = models.CharField(max_length=50) # 资源类型 + resource_id = models.IntegerField() # 资源 ID + status = models.CharField(max_length=20) # success/failed + confidence = models.FloatField() # AI 置信度 + reasoning = models.TextField() # AI 推理过程 + created_at = models.DateTimeField(auto_now_add=True) +``` + +## AI 权限系统 + +| 权限 | 说明 | 适用 AI | +|------|------|--------| +| `ai:read` | 读取数据 | 所有 AI | +| `ai:write` | 创建/更新数据 | 内容生成 AI、服务推荐 AI | +| `ai:review` | 审核内容 | 内容审核 AI | +| `ai:delete` | 删除数据 | 管理员 AI | +| `ai:batch` | 批量操作 | 所有 AI | +| `ai:analytics` | 访问分析数据 | 数据分析 AI | + +## 最佳实践 + +### 1. AI 应该 + +- ✅ 使用专用的 AI 账号(Service Account) +- ✅ 记录所有操作的 `agent_id` +- ✅ 提供操作的置信度 +- ✅ 提供操作的推理过程 +- ✅ 支持人工复核 +- ✅ 遵守速率限制 + +### 2. AI 不应该 + +- ❌ 使用人类用户的账号 +- ❌ 隐藏 AI 身份 +- ❌ 绕过审核流程 +- ❌ 无限制批量操作 +- ❌ 忽略错误处理 + +## 示例:AI 自动运营流程 + +```python +# 1. AI 内容生成器创建文章 +POST /api/articles/ +{ + "agent_id": "content-generator-ai", + "title": "成都美食攻略", + "region": 11, + "content": "...", + "auto_generated": true +} + +# 2. AI 审核器自动审核 +POST /api/articles/{id}/review/ +{ + "agent_id": "content-moderator-ai", + "action": "approve", + "confidence": 0.98, + "reasoning": "内容质量高,无明显问题" +} + +# 3. AI 推荐器添加到首页 +POST /api/featured/ +{ + "agent_id": "recommendation-ai", + "article_id": 123, + "reason": "热门文章,用户关注度高" +} + +# 4. AI 分析器生成报告 +GET /api/analytics/daily?agent=analytics-ai +``` + +## 配置示例 + +```python +# settings.py +AI_AGENTS = { + 'content-moderator-ai': { + 'name': '内容审核 AI', + 'secret': 'xxx', + 'permissions': ['review', 'approve', 'reject'], + 'rate_limit': 1000, # 每小时请求数 + }, + 'content-generator-ai': { + 'name': '内容生成 AI', + 'secret': 'xxx', + 'permissions': ['write'], + 'rate_limit': 100, + }, + # ... +} +``` + +## 未来扩展 + +- [ ] 自然语言查询接口 +- [ ] AI 之间的协作协议 +- [ ] 多 AI 投票决策机制 +- [ ] AI 操作可视化面板 +- [ ] AI 性能评估系统 +- [ ] AI 训练数据导出 + +--- + +**设计理念:** 城市手册不仅是给人用的,更是给 AI 用的。每一个 API、每一个功能、每一个流程,都要考虑 AI 如何自动化操作。 diff --git a/city-manual/backend/agents/__init__.py b/city-manual/backend/agents/__init__.py new file mode 100644 index 0000000..dc08e7a --- /dev/null +++ b/city-manual/backend/agents/__init__.py @@ -0,0 +1 @@ +default_app_config = 'agents.apps.AgentsConfig' diff --git a/city-manual/backend/agents/admin.py b/city-manual/backend/agents/admin.py new file mode 100644 index 0000000..cd7643a --- /dev/null +++ b/city-manual/backend/agents/admin.py @@ -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'] + }), + ) diff --git a/city-manual/backend/agents/apps.py b/city-manual/backend/agents/apps.py new file mode 100644 index 0000000..27fd97c --- /dev/null +++ b/city-manual/backend/agents/apps.py @@ -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' diff --git a/city-manual/backend/agents/migrations/0001_initial.py b/city-manual/backend/agents/migrations/0001_initial.py new file mode 100644 index 0000000..9422213 --- /dev/null +++ b/city-manual/backend/agents/migrations/0001_initial.py @@ -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')], + }, + ), + ] diff --git a/city-manual/backend/agents/migrations/__init__.py b/city-manual/backend/agents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/city-manual/backend/agents/models.py b/city-manual/backend/agents/models.py new file mode 100644 index 0000000..98103c4 --- /dev/null +++ b/city-manual/backend/agents/models.py @@ -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 diff --git a/city-manual/backend/agents/serializers.py b/city-manual/backend/agents/serializers.py new file mode 100644 index 0000000..03bcfc5 --- /dev/null +++ b/city-manual/backend/agents/serializers.py @@ -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 diff --git a/city-manual/backend/agents/views.py b/city-manual/backend/agents/views.py new file mode 100644 index 0000000..2796f15 --- /dev/null +++ b/city-manual/backend/agents/views.py @@ -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) diff --git a/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc b/city-manual/backend/city_manual/__pycache__/settings.cpython-312.pyc index efd19e8fdfe3668f05217066071da4a7bfe8f27b..ca4721ff8a8db42e081039456cd1b14132d33739 100644 GIT binary patch delta 78 zcmeB^o+izEnwOW00SL}C-_G2ykyo3Uk$18s^ELKcY>DZqc_o|qSYnwO#W&}$>#;B@ gPoB-4!RRxYg(sL@K!~Y<5EHTV%VnB&1kI8||!kc^8#aI}X gCNJjBVDz5M!xPNTFT~Wq_ep7U8jlVmiziSa08noeb^rhX diff --git a/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc b/city-manual/backend/city_manual/__pycache__/urls.cpython-312.pyc index d50a9b4c9da3701f3f0f46e2116060cfd34afe7b..fdf920f28671ac133bec257ba40a5f25468d118f 100644 GIT binary patch delta 1432 zcmah{%}>){7;m?-FV=N@Y!kWAYh`w&=^SB`vx}XO4cG$V}>zC zkC1wRlOB!ZK*D8-CZ0@8yv#s?){D`90Ah?+->&V@jb%MNeV*U%*WdF#@6)I5_nycn zl5}9``SIjidd-SqKh?r`oO<$$AT}c}@&E(POPw6e(43#~a{(s61(_fhVnR4(!l0v| z%b70NmAPh^oV}Y7z25Hc~<0+UM+<+JQf$tRE$^4oDI0FNq;}l`#4s=~73qcb;aTZ1s@ zOXCGxW5Btp?;dRb=n}hkNnPLPg$8Tv}DF>d**sz7~&hz{;W%p^V?ZBkr`WMUp>#i=x0y$wahlSoc3!yGH7`Rq79Z@^?C zd50AijnbV+-i31uJYPY`IaW+9%)$pSwIV|KA~Bg)EUfA*C{5*e6jy9>tN^nt2PH=t z8eHP%1<78?rDiP^Q7ggFsD~4zX-E2McyXv&A3&d ztQBJHintI{h-F?76+$hH4tkE1W|5cn|@;`96KJ! z$G&5hXVhbA!@hNO_r^MPfL;EgBV;KwRD7sHV+Q1UMHZbZGO8mbcXz2bUgTA5LT0RB zv}C7Cf%w+UVIclF5Z|3DPL+m6_5-6I#}A0v#3Z$4_553T*g6?E{wJ-sA^ delta 667 zcmdll+A5%NnwOW00SL~$xshqc%E0g##DM{@43P8r57R`ADfLVYsa&g}a#2#LQd#CO zg$O!@JC%Pm6NJymfI|&WssKSXys1KHYU&ZTKv^k#siJ6#FxBv)>Fl=PVvYNbrNuFI8sD>xYX0jHuw7AF`(bdclm0+D>K#~_KCypj3 z0hAMl$Vp0~$w>j_Sb(8wHF*`Yax!a_d@5U(1Evd#lv3q^Dmj3%PMESlQ5Q_nHPSHO zRq-+~M9EjmRLW?|ZZ=@yWt=>X)qk=-TcIRNVnL?9CgUyMl>Fq<+|<01_@ey$5jHX0t`7$Zer0FbKa&(_64Lgan59wf<)K7cf_iSR* zBn=<%Nxt{F=Xrkb^Z$MSQCR4t;P=me&k27orl@~mBKxys7LVFsF-{>0(IOS1`{@wV z&(IV@y-FeGJcUd=eaf0uT7FNNjCH?h%Sqw$ZwgZVW`^P|rwVcvj5T7A33#?st`&GRqLCR{V%90M!5w6f zjYxybPydh|IbzN!=85D6BAK--M;sNz3a98Naz(2l1}U`h6B@aYJ64u z%q+oL8kNLKb7afbz%B>uO@O{RR*~ah<*HWGs4BK4$G_@zV0-egx2|bRUfka%obq4vzEHMVy-9%$V$4qDMm@T@;_KYYf>t=|#`?Stp# z>&!axxBod>aR41yrxk6_XvHfee-+=&AIzE`!8~Y>9n8sL2a#TBg3+imuLsv3V>4=p zCojf9OMlVWA#a!NIFg<5eY&k(QH6lW>x_W9bf&x0Yt{?6s4BAoh38Zr=^Mg|5aN_E zc9jfjcG9X8IR^mduZcznQ2|q|aYo_|rKrqf`Qh;f8|~ zSi}D4M)=s?z11+;*iJ73c5Z<3WQ^$OPLRp&n9nEM=|$n_fG9+UGW&qn7Nl^4Dv440 z`d?C)KS)jg3O+kBf)EW7zRy21Sju)B5K>V@G-I1HQ}Z}+gn8+s)Xe=%%9u=FNb)Gl zA!In~jD`{JlB`Dhqr2&Acb;h2Nhg5@k6T zKc*+Xod3gpNLo!cX~Z%^Q@3|nf|$YZ(;m0$l~5rcEO)+J}e9y%=x#!Soqrw z-Fgfzj8agyL;|ux`gRdtyQ26u*O;ET>17u2b&b$X2958Ygl;xyEK2A-2958wgl+*k zYhCKn1uz6xs}V1lBt})@fdke*oca8yccX4+$w*>Z-N~|0!cmcjbpgx%EXs*Ef zNl;;s=>>?#4aQ3W0ooaxrElmUlkW(={3_Tmz2Kk_Y=@cHg#)iUI)*qUv+UzftH!dh z6Y8K(2=NA4a8Q;-o|6n}bFUnM{YLoXAx;##Vg3dA41`k-HpIXNIPK`R4B5y?b{fOikiqLwHQV z0V02R5orOZ&Qo6(6`y@|$xN46&mLZ~Qx4B1XVP9Bw^z@(E53G@YUREwBgt}Kyxcc! zo~_gRSxr(Bf6~2l3WQ8wY;ky%yy5= zxUcO&pVr%_1x6FT)0%5+&Rwf*Ynl9Ua$8G$TMHn)(#?R@apnYm3Cu8lonek*!1^8n z*b9JlT7B!}V6wh7Uf((mfGAPVYp%gLca>J%Ffo{{ZirVm0NP6jvHoG^P5c^Q5Pn6N zPK@R`M9{hbtwyWeJK377-5amnJB?;bv=gC3t)#i+IrkRL^L(0@*oEqj&|D~=fn7S8@UWWet)9TyMJzrZ=&ZLo4K^;ac!ByxzzA%Db7n} zL!zo%bG`OlLe_*mY>IoDrdwuzned#{Tx`CBIi4>S+Q7l$?>Jbx5ggo_=V0ac)i7jh zrQj}EOt6L1*y0Rmm&4h#5RlPH|FD zZa@QMZjw=HOq$oR7?C{!t8eZ5jVauw^Z<#Eh6uipESYGU{-2eiy^pQOXxe(knRL~~ MU3LGZFh%tL53AX#kN^Mx diff --git a/city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0002_initial.cpython-312.pyc deleted file mode 100644 index dc8f0c35fe277eba3ea06d7ab58767acfac42cd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1023 zcmZuw&1(}u6rY*Rem0FY7PYicHKDKvvsJGmlr}B+Q3P9|d+4&+nTAa_vvIPa#Di4m z!DDaz7nJtkUm$w&Ac#37j2FR^;33e8(37*fSryuWeeeC=*Sz`7?5FW@1Bmtc=fJ;% z0Qeq*Y{?_zcv%<+00RsI(1bQ@A{)UNqd>BA2&@1s?E#!4u#t4j8l?JAO0_L_ktzR( zfr+@$^y|!x{E)V}(hM;P+N)lgn98vcIDRLL13-WcF|ZMawuBLu>L}is$c_B}MX8N* zSQat0F>y`|Vo3DH`2efT!;2}*D%Mp{wO={nmD}5 zNIf}uetv%bmg7@D^4%Z~cp;4ljhH6(;7UvotqYx`i?Rf_pBF~;l~kKDmurDbw|S0s zf`IEGbp)X#B0SGX;6?;H)NK-uqR^BC^)cP4Cztr;EuRE<)huw$N&Jq(4aX6Aoqz~C z?>NsoZV=}Rq>kq0c}!Y_VnRKiv{`)3TwB0+Ic~;sF*KG#M*KQ`K)PJP4VTtKj&O}@ z$#{YSt}ZS-SzK7EawBY!(Svvcj$?{XA6qO8Bda1J)C=4lYbRve?Uw5i%k!h|&8ADm zR94OPw#69Mh&$`w%d!N$w=;Sxtu9kU+eE0sXa_>FghO#uN>^>ak zfj;xf=xftGZF+EJc5rcOP%Q1w^^2umvGlt9cJ2Msj~gF0`qlMbb-iER>{U1W)vaE2 z>r3aw5P&7LhE4#uw2n^HoNf#yrKr5T&u(Pfn+n%cIx4u9FqiT*_19TR|A|UQ6r0Iv z+4a~;T9t%#s@+ZR=^mRC5Q?uoE5uNO5dKyGG{1stC&~q=>>K@wa&Myi3kZ-j_yYuO B2QUBt diff --git a/city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0003_initial.cpython-312.pyc deleted file mode 100644 index a35dc102f117712fa18241442d077ce640b75531..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmZuw&1(}u6rY*Rem1Q&7OhsPn5gW*Y}KnMrA-Td6u~NV4_!7pld$P#w(f2$@gNm? z@Yq}b1*JXs7l@ub2x3kN<3;c!cnGv2^yKW93hltYdB5MAc{A_#WLLOPpb7wO`U z80d&;ZMPZNk?YftNo^k!FI;lcg{d9M!2Ub#>;M8xh=GYPGzE;X&_wacL~IrR-<0~e zfJGjYTjR&XAcjP3O&pE1#*X1mAHz)^<*r6?m$6o}K=Vl+x&Cb`D=C70NmAnD_vF?WtjllOKqsk-H@$3y_!w=TN zb=x6^<3`bomE)FH2 zULGjrp(sp9oA-xBpq_uF^_8ifGBr3eJve!8P%3X<>6gmAQu%e|?ehC4A6GuC^lOiM zwa5M1YOl81uQhtL#+S~EAprAe6&(O@%0dTnLDhzWRFYoa4a)iPy2R8}jw-Gv%B6ap z{YzHUAElZL6iwj)9v6Yhme2aG&e&5Lik$(Q2z=p9!Mvkw5|2W NE4}f`FTg>P;16;@5a9p- diff --git a/city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc b/city-manual/backend/content/migrations/__pycache__/0004_initial.cpython-312.pyc deleted file mode 100644 index 00d714614a47f4d1d8f32a49970f8c0455c6f7ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4964 zcmcgwUrbY17(e&6y}d0(Xkj7(ih@8N)@rtgO%0Jr!8wFQs0-c3-KFPHZn?b|Pj4X@ z%~;|xUADxh?Zr%eSu_yc_T*TWY)^Ywvb5Plo3n>4nIKE{z#?XePdn%KRzcgO&5))! z|Ie?#?{~iQ-S772>S{NjpI`pYDPPtC@F(jOJ`St#;52P~4j_P>3KCp|<5-(l`GhTE zOV}fJvu{%!5rGH&0NO7C=s>pkjsnn=dcl@&N1Ssuzwj69_&FjbkzpuWEa~ToJE6p| zY$&>xBtk-mNKFn#p8x>RV09i~7h{D$go7Z$LoQ;2JhVYO50E`>TD5T-07xjxh|8~v z*(0L4)3p=&kmLYe1=d*Lj6W{&pXuQ72SaXrFJtS0-UH2EU(gE8i zxy1tue148yXP=Y}0+t@Ccii`7PabDjP!;yP7G(MD*(=ENvwW^0~<2M->S6iqP{ zS)~&+tHU!&6eU@r2H47|t{F%(2wVA5VI>h*x~Ij6Jw-QAIJ1}Z+lWioB%1#r0}&5K zs%#)AX>tM)-q7i^+sqF0BX)D#F`*z861%EQA>x#=p+r?g9ITr>S7ZHpz#8{Eh+|xp zwJBoPQmTq+_6x9=6)XcJPK*7yT?I1&6G%2v7(r`*h!gURjulEz$ZJ46g%K!?&z;+0 zDHE_}U4_{slOmB|QixU~nPRbY<+G3OFI@KvxRLQ-dF9izJTnN7>q(S1TaZ)6q>fWe z;MF@9S8p$^&VT5)KeY)nCrEi{asTqStCzo7x%%ny$2W**36cNld*5HBdx?*UN{>Lt z5=Ju$nx#kLwOUYmyraehyL9jFcgweLtX#i)Z)wT=ftAbi%U^%7{K?YtwQGJGW^Vwv zg%wS-@bj3Z#iS;sDQ8niGV~ZSCJ`C{kfXn=SguTW5u zh&>SYG1tK?eI@Qb9U~>Cy@BS40OPV2(+Lm9iD)__^>yMr-ZOl>r#D2z;n0XQ7#$qaM-kL#4?Py^=T$1yB@c|1BwkEy&1&1ZUh5#MvJQQY;ab`Q^{#Lih@zan0qlH zYt+EtxE!6LCIl^~4L*4*3ewB-a^dm>rsuGe!kUFh8_5-_#YOPP4&QwLngCqwIgx(b za$@HtcUJ7oh@CgZY}c_&*Rfy4o;8=FUYH+Ps|BuRHr*Mt>a5hQgUpI2#(vgvPR=iA-oBojSh;z-#<4&)`P33ho^f?%h&w z<(Z7qaZF8~;*K+uS;aW^76sjx^EQ@gGPQSx>*Z_I|p@<2{`wUX^%SXQ`Kfgp@FTWr) zFF7Z%T)#ZOD7&~IF*#K~IkTivH#ae_G%-g%DKR-aH7`X!IX|x?HLpZJH#5B`u_QA; uuUJ1mJ~J<~BtBlRpz;@oO>TZlX-=wL5i8JeMj$Q*F+MUgGBOr116csMoi08A diff --git a/city-manual/backend/init_agents.py b/city-manual/backend/init_agents.py new file mode 100644 index 0000000..cb31b06 --- /dev/null +++ b/city-manual/backend/init_agents.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +初始化 AI 代理 - 创建默认的 AI 代理账号 +""" + +import os +import sys +import django + +# 设置 Django 环境 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'city_manual.settings') +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +django.setup() + +from agents.models import AIAgent + +def create_default_agents(): + """创建默认的 AI 代理""" + + agents = [ + { + 'agent_id': 'content-moderator-ai', + 'name': '内容审核 AI', + 'description': '负责审核用户提交的文章和服务内容', + 'permissions': ['read', 'review', 'approve', 'write'], + 'rate_limit': 1000, + }, + { + 'agent_id': 'content-generator-ai', + 'name': '内容生成 AI', + 'description': '自动生成城市介绍、旅游攻略等内容', + 'permissions': ['read', 'write'], + 'rate_limit': 100, + }, + { + 'agent_id': 'service-curator-ai', + 'name': '服务推荐 AI', + 'description': '自动发现和推荐本地特色服务', + 'permissions': ['read', 'write'], + 'rate_limit': 100, + }, + { + 'agent_id': 'analytics-ai', + 'name': '数据分析 AI', + 'description': '分析用户行为和平台数据', + 'permissions': ['read', 'analytics'], + 'rate_limit': 500, + }, + { + 'agent_id': 'admin-ai', + 'name': '管理员 AI', + 'description': '全自动管理员,拥有所有权限', + 'permissions': ['read', 'write', 'review', 'delete', 'batch', 'analytics'], + 'rate_limit': 10000, + }, + ] + + import uuid + + for agent_data in agents: + agent, created = AIAgent.objects.get_or_create( + agent_id=agent_data['agent_id'], + defaults={ + 'name': agent_data['name'], + 'description': agent_data['description'], + 'secret_key': uuid.uuid4().hex, + 'permissions': agent_data['permissions'], + 'rate_limit': agent_data['rate_limit'], + } + ) + + if created: + print(f"✅ 创建 AI 代理:{agent.name} ({agent.agent_id})") + print(f" 密钥:{agent.secret_key}") + print(f" 权限:{', '.join(agent.permissions)}") + print() + else: + print(f"⚠️ AI 代理已存在:{agent.name}") + print(f" 密钥:{agent.secret_key}") + print() + +if __name__ == '__main__': + print("=" * 60) + print("🤖 初始化 AI 代理系统") + print("=" * 60) + print() + + create_default_agents() + + print("=" * 60) + print("✅ AI 代理初始化完成!") + print() + print("⚠️ 请妥善保管密钥,用于 AI 代理认证") + print("=" * 60) diff --git a/city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/regions/management/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 9a27e174e8c6dcbbf2f48f84447a64551758bf70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmX@j%ge<81n1t|$OO@kK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%SXQ`Kfgp@FTWr) zFF7Z%T)#ZOD7&~IF*#K~IkTivH#ae_G%-g%DKR-aH7`ZKC^bDZKd)FHD4dv{nwy$e tq8}fhnU`4-AFo$X`HRCQH$SB`C)KWq6=*mk5Ep|OADI~$8H<>KEC8}OE*by; diff --git a/city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/regions/management/commands/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5f3a777742c74fbeb6b4f51f27adcbd3bea9a266..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmX@j%ge<81n1t|$OO@kK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^D^$NIKfgp@FTWr) zFF7Z%T)#ZOD7&~IF*#K~IkTivH#ae_G%-g%DKR-aH7`ZKC^bDZKd)FHD4dv{nwy$e zqMw|f3*@B~>&M4u=4F<|$LkeT{^GF7%}*)KNwq6t1=_+0#Kj=SM`lJw#v*1Q3jn3F BF{A(h diff --git a/city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc b/city-manual/backend/regions/management/commands/__pycache__/seed_data.cpython-312.pyc deleted file mode 100644 index 4e0f00065cdef2727500b381e6a812ce49216f39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11557 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 diff --git a/city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/regions/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 6bb25e8ec068cdfcd22f38febe724efe66229042..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5211 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 diff --git a/city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc b/city-manual/backend/services/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index a0c1e8d374b6af4510ce71f98649c885fda64072..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3632 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 DD=XBQ diff --git a/city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/services/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0cac2bd481ac046b70c183272686c8158bb63a1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 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 diff --git a/city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc b/city-manual/backend/users/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3e50c9da82a6554eddd1bdb8f9e86b88ba8a101e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171 zcmX@j%ge<81n1t|$OO@kK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%S*o~Kfgp@FTWr) zFF7Z%T)#ZOD7&~IF*#K~IkTivH#ae_G%-g%DKR-aH7`ZKv^ce>SU)#2y(qCHGe56b sKR!M)FS8^*Uaz3?7l%!5eoARhs$CH)&}c>=E(S3^GBYwV7BK@^08Y~_a{vGU