Fix Signal messages and replies

This commit is contained in:
2026-03-03 15:51:58 +00:00
parent 56c620473f
commit d6bd56dace
31 changed files with 3317 additions and 668 deletions

View File

@@ -7,9 +7,11 @@ from django.db import transaction
from django.db.models import Avg, Count, Q, Sum
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views import View
from core.commands.policies import BP_VARIANT_KEYS, BP_VARIANT_META, ensure_variant_policies_for_profile
from core.models import (
AIRunLog,
BusinessPlanDocument,
@@ -17,12 +19,29 @@ from core.models import (
CommandAction,
CommandChannelBinding,
CommandProfile,
CommandVariantPolicy,
TranslationBridge,
TranslationEventLog,
)
from core.translation.engine import parse_quick_mode_title
def _channel_variants(service: str, identifier: str) -> list[str]:
value = str(identifier or "").strip()
if not value:
return []
variants = [value]
svc = str(service or "").strip().lower()
if svc == "whatsapp":
bare = value.split("@", 1)[0].strip()
if bare and bare not in variants:
variants.append(bare)
group = f"{bare}@g.us" if bare else ""
if group and group not in variants:
variants.append(group)
return variants
class CommandRoutingSettings(LoginRequiredMixin, View):
template_name = "pages/command-routing.html"
@@ -35,12 +54,71 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
row.save(update_fields=["position", "updated_at"])
return rows
@staticmethod
def _redirect_with_scope(request):
service = str(request.GET.get("service") or request.POST.get("service") or "").strip()
identifier = str(
request.GET.get("identifier") or request.POST.get("identifier") or ""
).strip()
if service and identifier:
return redirect(
f"{reverse('command_routing')}?service={service}&identifier={identifier}"
)
return redirect("command_routing")
def _context(self, request):
profiles = (
profiles_qs = (
CommandProfile.objects.filter(user=request.user)
.prefetch_related("channel_bindings", "actions")
.prefetch_related("channel_bindings", "actions", "variant_policies")
.order_by("slug")
)
scope_service = str(request.GET.get("service") or "").strip().lower()
scope_identifier = str(request.GET.get("identifier") or "").strip()
scope_variants = _channel_variants(scope_service, scope_identifier)
profiles = list(profiles_qs)
preview_profile_id = str(request.GET.get("preview_profile_id") or "").strip()
for profile in profiles:
policies = ensure_variant_policies_for_profile(profile)
if str(profile.slug or "").strip() == "bp":
keys = BP_VARIANT_KEYS
else:
keys = ("default",)
profile.variant_rows = []
for key in keys:
row = policies.get(key)
if row is None:
continue
meta = BP_VARIANT_META.get(key, {})
profile.variant_rows.append(
{
"variant_key": key,
"variant_label": str(meta.get("name") or key),
"trigger_token": str(meta.get("trigger_token") or profile.trigger_token or ""),
"template_supported": bool(meta.get("template_supported")),
"warn_verbatim_plan": bool(
key in {"bp", "bp_set_range"}
and str(getattr(row, "generation_mode", "") or "") == "verbatim"
and bool(getattr(row, "send_plan_to_egress", False))
),
"row": row,
}
)
bindings = list(profile.channel_bindings.all())
if scope_service and scope_variants:
profile.visible_bindings = [
row
for row in bindings
if str(row.service or "").strip().lower() == scope_service
and str(row.channel_identifier or "").strip() in scope_variants
]
else:
profile.visible_bindings = bindings
profile.enabled_egress_bindings = [
row
for row in bindings
if str(row.direction or "").strip() == "egress" and bool(row.enabled)
]
profile.preview_mode = preview_profile_id and str(profile.id) == preview_profile_id
documents = BusinessPlanDocument.objects.filter(user=request.user).order_by(
"-updated_at"
)[:30]
@@ -50,6 +128,11 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
"channel_services": ("web", "xmpp", "signal", "whatsapp"),
"directions": ("ingress", "egress", "scratchpad_mirror"),
"action_types": ("extract_bp", "post_result", "save_document"),
"command_choices": (("bp", "Business Plan (bp)"),),
"scope_service": scope_service,
"scope_identifier": scope_identifier,
"scope_variants": scope_variants,
"preview_profile_id": preview_profile_id,
}
def get(self, request):
@@ -59,7 +142,12 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
action = str(request.POST.get("action") or "").strip()
if action == "profile_create":
slug = str(request.POST.get("slug") or "bp").strip().lower() or "bp"
slug = (
str(request.POST.get("command_slug") or request.POST.get("slug") or "bp")
.strip()
.lower()
or "bp"
)
profile, _ = CommandProfile.objects.get_or_create(
user=request.user,
slug=slug,
@@ -74,6 +162,11 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
"template_text": str(request.POST.get("template_text") or ""),
},
)
profile.name = str(request.POST.get("name") or profile.name).strip() or profile.name
if slug == "bp":
profile.trigger_token = "#bp#"
profile.template_text = str(request.POST.get("template_text") or profile.template_text or "")
profile.save(update_fields=["name", "trigger_token", "template_text", "updated_at"])
CommandAction.objects.get_or_create(
profile=profile,
action_type="extract_bp",
@@ -89,7 +182,8 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
action_type="post_result",
defaults={"enabled": True, "position": 2},
)
return redirect("command_routing")
ensure_variant_policies_for_profile(profile)
return self._redirect_with_scope(request)
if action == "profile_update":
profile = get_object_or_404(
@@ -106,12 +200,11 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
profile.reply_required = bool(request.POST.get("reply_required"))
profile.exact_match_only = bool(request.POST.get("exact_match_only"))
profile.template_text = str(request.POST.get("template_text") or "")
profile.visibility_mode = (
str(request.POST.get("visibility_mode") or "status_in_source").strip()
or "status_in_source"
)
# Legacy field retained for compatibility only.
profile.visibility_mode = profile.visibility_mode or "status_in_source"
profile.save()
return redirect("command_routing")
ensure_variant_policies_for_profile(profile)
return self._redirect_with_scope(request)
if action == "profile_delete":
profile = get_object_or_404(
@@ -120,7 +213,7 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
user=request.user,
)
profile.delete()
return redirect("command_routing")
return self._redirect_with_scope(request)
if action == "binding_create":
profile = get_object_or_404(
@@ -137,7 +230,7 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
).strip(),
enabled=bool(request.POST.get("enabled") or "1"),
)
return redirect("command_routing")
return self._redirect_with_scope(request)
if action == "binding_delete":
binding = get_object_or_404(
@@ -146,7 +239,7 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
profile__user=request.user,
)
binding.delete()
return redirect("command_routing")
return self._redirect_with_scope(request)
if action == "action_update":
row = get_object_or_404(
@@ -160,7 +253,7 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
row.save(update_fields=["enabled", "position", "updated_at"])
else:
row.save(update_fields=["enabled", "updated_at"])
return redirect("command_routing")
return self._redirect_with_scope(request)
if action == "action_move":
row = get_object_or_404(
@@ -170,26 +263,74 @@ class CommandRoutingSettings(LoginRequiredMixin, View):
)
direction = str(request.POST.get("direction") or "").strip().lower()
if direction not in {"up", "down"}:
return redirect("command_routing")
return self._redirect_with_scope(request)
with transaction.atomic():
ordered = self._normalize_action_positions(row.profile)
action_ids = [entry.id for entry in ordered]
try:
idx = action_ids.index(row.id)
except ValueError:
return redirect("command_routing")
return self._redirect_with_scope(request)
target_idx = idx - 1 if direction == "up" else idx + 1
if target_idx < 0 or target_idx >= len(ordered):
return redirect("command_routing")
return self._redirect_with_scope(request)
other = ordered[target_idx]
current_pos = ordered[idx].position
ordered[idx].position = other.position
other.position = current_pos
ordered[idx].save(update_fields=["position", "updated_at"])
other.save(update_fields=["position", "updated_at"])
return redirect("command_routing")
return self._redirect_with_scope(request)
return redirect("command_routing")
if action == "variant_policy_update":
profile = get_object_or_404(
CommandProfile,
id=request.POST.get("profile_id"),
user=request.user,
)
variant_key = str(request.POST.get("variant_key") or "").strip()
policy = get_object_or_404(
CommandVariantPolicy,
profile=profile,
variant_key=variant_key,
)
policy.enabled = bool(request.POST.get("enabled"))
mode = str(request.POST.get("generation_mode") or "verbatim").strip().lower()
policy.generation_mode = mode if mode in {"ai", "verbatim"} else "verbatim"
policy.send_plan_to_egress = bool(request.POST.get("send_plan_to_egress"))
policy.send_status_to_source = bool(request.POST.get("send_status_to_source"))
policy.send_status_to_egress = bool(request.POST.get("send_status_to_egress"))
policy.store_document = bool(request.POST.get("store_document"))
policy.save()
return self._redirect_with_scope(request)
if action == "variant_policy_reset_defaults":
profile = get_object_or_404(
CommandProfile,
id=request.POST.get("profile_id"),
user=request.user,
)
profile.variant_policies.all().delete()
ensure_variant_policies_for_profile(profile)
return self._redirect_with_scope(request)
if action == "variant_preview":
profile = get_object_or_404(
CommandProfile,
id=request.POST.get("profile_id"),
user=request.user,
)
ensure_variant_policies_for_profile(profile)
service = str(request.GET.get("service") or request.POST.get("service") or "").strip()
identifier = str(
request.GET.get("identifier") or request.POST.get("identifier") or ""
).strip()
query = f"?preview_profile_id={profile.id}"
if service and identifier:
query += f"&service={service}&identifier={identifier}"
return redirect(f"{reverse('command_routing')}{query}")
return self._redirect_with_scope(request)
class TranslationSettings(LoginRequiredMixin, View):