diff --git a/agent_core/governance.py b/agent_core/governance.py index fcdfec5..ea21e3d 100644 --- a/agent_core/governance.py +++ b/agent_core/governance.py @@ -2,6 +2,7 @@ from pathlib import Path import yaml from django.conf import settings +from django.db.utils import OperationalError, ProgrammingError def governance_defaults() -> dict: @@ -81,8 +82,61 @@ def read_governance_yaml() -> dict: def load_governance_config() -> dict: defaults = governance_defaults() config = read_governance_yaml() + db_config = load_governance_config_from_db() for key, default_value in defaults.items(): - configured_value = config.get(key) + configured_value = db_config.get(key) or config.get(key) if isinstance(default_value, list) and configured_value: defaults[key] = configured_value return defaults + + +def load_governance_config_from_db() -> dict: + try: + from apps.platform_ui.models import FeishuNotifyConfig, OwnerMapping, WordTemplateMapping + except Exception: + return {} + + try: + owner_mappings = [ + { + "owner_role": item.owner_role, + "owner_name": item.owner_name, + "department": item.department, + "chapter_scope": item.chapter_scope, + "risk_scope": item.risk_scope, + "feishu_user_id": item.feishu_user_id, + "feishu_open_id": item.feishu_open_id, + "feishu_name": item.feishu_name, + "notify_enabled": "是" if item.notify_enabled else "否", + } + for item in OwnerMapping.objects.filter(is_active=True) + ] + feishu_configs = [ + { + "config_name": item.config_name, + "notify_reason": item.notify_reason, + "channel": item.channel, + "message_template": item.message_template, + "status": item.status, + } + for item in FeishuNotifyConfig.objects.filter(is_active=True) + ] + template_mappings = [ + { + "template_name": item.template_name, + "output_type": item.output_type, + "version": item.version, + "placeholder_count": item.placeholder_count, + "status": item.status, + "field_mapping_summary": item.field_mapping_summary, + } + for item in WordTemplateMapping.objects.filter(is_active=True) + ] + except (OperationalError, ProgrammingError, RuntimeError): + return {} + + return { + "owner_mappings": owner_mappings, + "feishu_configs": feishu_configs, + "template_mappings": template_mappings, + } diff --git a/apps/platform_ui/admin.py b/apps/platform_ui/admin.py new file mode 100644 index 0000000..4dd1d2c --- /dev/null +++ b/apps/platform_ui/admin.py @@ -0,0 +1,40 @@ +from django.contrib import admin + +from .models import FeishuNotifyConfig, OwnerMapping, WordTemplateMapping + + +@admin.register(OwnerMapping) +class OwnerMappingAdmin(admin.ModelAdmin): + list_display = ( + "owner_role", + "owner_name", + "department", + "chapter_scope", + "risk_scope", + "feishu_user_id", + "notify_enabled", + "is_active", + ) + list_filter = ("notify_enabled", "is_active", "department") + search_fields = ("owner_role", "owner_name", "department", "chapter_scope", "risk_scope") + + +@admin.register(FeishuNotifyConfig) +class FeishuNotifyConfigAdmin(admin.ModelAdmin): + list_display = ("config_name", "notify_reason", "channel", "status", "is_active") + list_filter = ("notify_reason", "status", "is_active") + search_fields = ("config_name", "channel", "message_template") + + +@admin.register(WordTemplateMapping) +class WordTemplateMappingAdmin(admin.ModelAdmin): + list_display = ( + "template_name", + "output_type", + "version", + "placeholder_count", + "status", + "is_active", + ) + list_filter = ("output_type", "status", "is_active") + search_fields = ("template_name", "version", "field_mapping_summary") diff --git a/apps/platform_ui/migrations/0001_initial.py b/apps/platform_ui/migrations/0001_initial.py new file mode 100644 index 0000000..af572db --- /dev/null +++ b/apps/platform_ui/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 5.2.14 on 2026-06-03 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="FeishuNotifyConfig", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("config_name", models.CharField(max_length=100)), + ( + "notify_reason", + models.CharField( + choices=[ + ("task_completed", "task_completed"), + ("task_failed", "task_failed"), + ], + db_index=True, + max_length=32, + ), + ), + ("channel", models.CharField(blank=True, max_length=100)), + ("message_template", models.CharField(blank=True, max_length=255)), + ("status", models.CharField(blank=True, max_length=32)), + ("is_active", models.BooleanField(db_index=True, default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "ordering": ["notify_reason", "id"], + }, + ), + migrations.CreateModel( + name="OwnerMapping", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("owner_role", models.CharField(db_index=True, max_length=100)), + ("owner_name", models.CharField(max_length=100)), + ("department", models.CharField(blank=True, max_length=100)), + ("chapter_scope", models.CharField(blank=True, max_length=100)), + ("risk_scope", models.CharField(blank=True, max_length=255)), + ("feishu_user_id", models.CharField(blank=True, max_length=100)), + ("feishu_open_id", models.CharField(blank=True, max_length=100)), + ("feishu_name", models.CharField(blank=True, max_length=100)), + ("notify_enabled", models.BooleanField(default=True)), + ("is_active", models.BooleanField(db_index=True, default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "ordering": ["owner_role", "id"], + }, + ), + migrations.CreateModel( + name="WordTemplateMapping", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("template_name", models.CharField(max_length=100)), + ( + "output_type", + models.CharField( + default="registration_word_export_report", max_length=100 + ), + ), + ("version", models.CharField(blank=True, max_length=50)), + ("placeholder_count", models.PositiveIntegerField(default=0)), + ("status", models.CharField(blank=True, max_length=32)), + ("field_mapping_summary", models.CharField(blank=True, max_length=255)), + ("is_active", models.BooleanField(db_index=True, default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "ordering": ["template_name", "id"], + }, + ), + ] diff --git a/apps/platform_ui/migrations/__init__.py b/apps/platform_ui/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/platform_ui/models.py b/apps/platform_ui/models.py new file mode 100644 index 0000000..f404949 --- /dev/null +++ b/apps/platform_ui/models.py @@ -0,0 +1,78 @@ +from django.db import models + + +class OwnerMapping(models.Model): + """ + 责任人映射。 + + 首版以 Django Admin 作为手工维护入口,字段口径与通知载荷保持一致。 + """ + + owner_role = models.CharField(max_length=100, db_index=True) + owner_name = models.CharField(max_length=100) + department = models.CharField(max_length=100, blank=True) + chapter_scope = models.CharField(max_length=100, blank=True) + risk_scope = models.CharField(max_length=255, blank=True) + feishu_user_id = models.CharField(max_length=100, blank=True) + feishu_open_id = models.CharField(max_length=100, blank=True) + feishu_name = models.CharField(max_length=100, blank=True) + notify_enabled = models.BooleanField(default=True) + is_active = models.BooleanField(default=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["owner_role", "id"] + + def __str__(self) -> str: + return f"{self.owner_role}-{self.owner_name}" + + +class FeishuNotifyConfig(models.Model): + """ + 飞书通知配置。 + """ + + NOTIFY_REASON_COMPLETED = "task_completed" + NOTIFY_REASON_FAILED = "task_failed" + NOTIFY_REASON_CHOICES = [ + (NOTIFY_REASON_COMPLETED, "task_completed"), + (NOTIFY_REASON_FAILED, "task_failed"), + ] + + config_name = models.CharField(max_length=100) + notify_reason = models.CharField(max_length=32, choices=NOTIFY_REASON_CHOICES, db_index=True) + channel = models.CharField(max_length=100, blank=True) + message_template = models.CharField(max_length=255, blank=True) + status = models.CharField(max_length=32, blank=True) + is_active = models.BooleanField(default=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["notify_reason", "id"] + + def __str__(self) -> str: + return self.config_name + + +class WordTemplateMapping(models.Model): + """ + Word 模板与字段映射摘要。 + """ + + template_name = models.CharField(max_length=100) + output_type = models.CharField(max_length=100, default="registration_word_export_report") + version = models.CharField(max_length=50, blank=True) + placeholder_count = models.PositiveIntegerField(default=0) + status = models.CharField(max_length=32, blank=True) + field_mapping_summary = models.CharField(max_length=255, blank=True) + is_active = models.BooleanField(default=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["template_name", "id"] + + def __str__(self) -> str: + return self.template_name diff --git a/tests/test_platform_ui.py b/tests/test_platform_ui.py index 2cb59a7..8a8bbd9 100644 --- a/tests/test_platform_ui.py +++ b/tests/test_platform_ui.py @@ -1,7 +1,9 @@ from django.urls import reverse from django.test import override_settings +from django.contrib import admin from apps.platform_ui.services import get_platform_demo_context +from apps.platform_ui.models import FeishuNotifyConfig, OwnerMapping, WordTemplateMapping def test_knowledge_base_page_shows_governance_sections(client): @@ -100,3 +102,47 @@ template_mappings: assert "Word 模板与字段映射" in content assert "风险摘要导出模板" in content assert "风险等级 / 产品名称 / 责任人" in content + + +def test_get_platform_demo_context_prefers_database_governance_entries(db): + OwnerMapping.objects.create( + owner_role="数据库负责人", + owner_name="周八", + department="注册事务部", + chapter_scope="CH5", + risk_scope="数据库覆盖", + feishu_user_id="ou_db_1", + feishu_open_id="on_db_1", + feishu_name="周八", + notify_enabled=True, + is_active=True, + ) + FeishuNotifyConfig.objects.create( + config_name="数据库通知配置", + notify_reason="task_failed", + channel="群机器人", + message_template="异常摘要 + @处理人", + status="启用", + is_active=True, + ) + WordTemplateMapping.objects.create( + template_name="数据库模板", + output_type="registration_word_export_report", + version="V3.0", + placeholder_count=16, + status="启用", + field_mapping_summary="产品名称 / 规格 / 风险等级", + is_active=True, + ) + + context = get_platform_demo_context() + + assert context["owner_mappings"][0]["owner_name"] == "周八" + assert context["feishu_configs"][0]["config_name"] == "数据库通知配置" + assert context["template_mappings"][0]["template_name"] == "数据库模板" + + +def test_governance_models_are_registered_in_admin(): + assert OwnerMapping in admin.site._registry + assert FeishuNotifyConfig in admin.site._registry + assert WordTemplateMapping in admin.site._registry