跳转至

侵权威胁模块 · Threats Module

Status: shipped (Phase 4, Steps 1–10) Frontend route: /threats Backend prefix: /api/v1/threats

10-step upgrade modelled on the legal-document module. Each step ships independently, end-to-end (backend + UI), and is independently verifiable.


1. Module purpose

A single workspace for game-IP owners to triage, evidence, and act on infringement signals flowing into the portal:

  • Detect → Triage → Evidence → Action → Escalate → Close (6-stage pipeline)
  • One-click pivots: 调取证据链 / 转法务起草 / 升级为私服 · 关联运营者
  • AI summary + live activity feed for situational awareness
  • Bulk close / restore / CSV export
  • Multi-tenant: tenant_admin sees only its portal_publications; super_admin sees the global pool.

2. Step ledger (what shipped, in order)

Step Scope Deliverable
0 Shared primitives lib/tone.ts + lib/format.ts + 5 components/ui building blocks (ModalShell, KpiTile, …)
1 Hero rewrite 6 KpiTile cards + 24h live banner + 7-day MiniSparkline (uses prev_7d for delta colour)
2 Pipeline ThreatPipeline.tsx — 6 stage cards with flow connectors; clicking a stage filters the list
3 Filter bar MultiSelectPopover + DateRangePopover + SavedQueriesDropdown (localStorage); URL sync
4 Bulk operations Sticky toolbar (selected count, 批量关闭, 批量恢复, 导出 CSV); role-based routing via POST /threats/bulk
5 Detail drawer ThreatDetailDrawer.tsx — KPIs + metadata + 3 action buttons; replaces old preview modal
6 Evidence sub-modal ThreatEvidenceModal.tsx + /threats/:id/evidence (+ ZIP bundle with manifest.json + SHA-256)
7 Legal handoff sub-modal ThreatToLegalModal.tsx + POST /legal-docs/draft (transactional: doc + publication)
8 Promote sub-modal ThreatPromoteModal.tsx + POST /threats/:id/promote (private_server OR actor; jsonb stamping)
9 AI summary + activity feed ThreatAISummary.tsx + ThreatActivityFeed.tsx + GET /threats/ai-summary + /threats/activity
10 Verification + docs Route-registration test + this reference doc

3. API reference

List & filter

GET /api/v1/threats

Query Type Notes
search string ILIKE across title / content_preview / author
platform string exact match
stage enum detected | triaged | evidenced | actioned | escalated | closed
risk_level csv critical,high,medium,low
threat_type csv
platforms csv
games csv
date_from / date_to RFC3339
page / page_size int default 1 / 20

Response: { items: PortalThreat[], total, page, page_size }

Stats

GET /api/v1/threats/stats — powers Hero, Pipeline, Filter facets

{
  "total": 22918,
  "by_type":     [{"threat_type": "private_server", "count": 8112}, ...],
  "by_risk":     [{"risk_level": "high", "count": 4520}, ...],
  "by_stage":    {"detected": 12, "triaged": 4, ...},  // always all 6 keys
  "by_platform": [{"value": "telegram", "count": 5403}, ...],
  "by_game":     [{"value": "MIR4", "count": 4112}, ...],
  "new_24h": 137, "new_7d": 812, "prev_7d": 691,
  "sparkline_7d": [82, 90, 110, 132, 95, 121, 137]   // oldest → newest
}

Time bucketing: tenant scope → portal_publications.published_at · super_admin scope → portal_threats.synced_at.

Bulk

POST /api/v1/threats/bulk — role-aware

// body
{
  "ids": ["uuid", "..."],
  "action": "close" | "restore" | "set_status",
  "status": "closed_resolved"   // only with set_status
}
// response
{ "rows_affected": 3 }
  • tenant_admin close/restore → mutates portal_publications.revoked_at
  • super_admin close/restore → mutates portal_threats.status

Evidence

Method Path Returns
GET /threats/:id/evidence { items: ThreatEvidenceItem[], total, anchored, tsa }
GET /threats/:id/evidence-bundle ZIP — threat.json + evidence-items.json + README.md + manifest.json (SHA-256 chain)

Author-bridge: items merge a synthetic "self" snapshot of the threat with every portal_evidence_chain whose actor.name matches threat.author (case-insensitive).

POST /api/v1/legal-docs/draft — transactional: portal_legal_docs row + portal_publications row

// body
{
  "from_threat_id": "uuid",
  "document_type":  "dmca_takedown" | "cease_desist" | "preservation_letter" | "trademark_notice" | "infringement_notice",
  "target_url":     "https://...",
  "recipient":      "Telegram Trust & Safety <[email protected]>",
  "language":       "zh" | "en",
  "evidence_ids":   ["..."],     // informational, embedded in body
  "instructions":   "free-form additional context"
}
// response
{ "doc": PortalLegalDoc, "publication_id": "uuid"|null }

Smart defaults if a field is empty:

  • document_typetemplateForThreatType(threat.threat_type)
  • recipientplatformRecipient(threat.platform) (10+ platforms mapped)
  • title[{game}] {label} · {threat.title}
  • content ← templated body (中/英文 stub with 【AI 起草占位】 line)

Promote (Step 8)

POST /api/v1/threats/:id/promote

// body — mode determines which fields are used
{
  "mode": "private_server" | "actor",

  // private_server fields
  "server_name":   "...",
  "domain":        "...",       // default: extracted from threat.source_url
  "ip_address":    "...",
  "geo_country":   "CN",
  "risk_level":    "high",
  "notes":         "...",

  // actor fields
  "actor_id":         "uuid",   // when set: link to existing
  "new_actor_name":   "...",    // when actor_id is empty: create
  "actor_aliases":    ["..."],
  "profile_summary":  "..."
}
// response
{
  "mode": "private_server",
  "private_server": PortalPrivateServer | null,
  "actor":          PortalActor | null,
  "publication_id": "uuid" | null
}

Side effects:

  • New portal_private_servers row OR new/linked portal_actors row
  • portal_publications row (skipped for super_admin to allow intel-dispatch routing)
  • portal_threats.metadatajsonb_set with promoted_to_*_id, promoted_at, promoted_mode

GET /api/v1/threats/:id/promote-candidates — drives "link existing operator" autocomplete; returns up to 10 actors whose name / aliases match threat.author (ILIKE).

Activity feed & AI summary (Step 9)

GET /api/v1/threats/activity?limit=30

{ "items": [{
  "id":         "threat:uuid" | "legal:uuid" | "promote:uuid",
  "kind":       "threat_new" | "legal_draft" | "promotion" | "actor_link",
  "title":      "...",
  "detail":     "...",
  "tone":       "info" | "warning" | "review" | "ok" | "danger",
  "timestamp":  "RFC3339",
  "source_id":  "...",
  "source_type":"threat" | "legal_doc"
}] }

3 SQL queries unioned + merged in Go: threats (recent), legal drafts (recent), threats with metadata ? 'promoted_at'.

GET /api/v1/threats/ai-summary

{
  "generated_at": "RFC3339",
  "model": "stats-derived/v1",
  "headline":    "本期共监测到 22918 条侵权威胁,过去 24h 新增 137 条,整体 7 日趋势上行。",
  "paragraphs":  ["重点风险面:telegram 平台...", "对比上一个 7 天周期..."],
  "insights":    ["Top 2 平台:telegram (5403) / youtube (3120)", ...],
  "suggestions": ["批量勾选高风险威胁 → 批量「转法务起草」", ...]
}

Currently model: stats-derived/v1 — pure templating over ThreatStats. Hot-swap to DGX vLLM or dashscope by replacing the body of BuildThreatAISummary; payload shape is forward-compatible.


4. Frontend component tree

src/app/(portal)/threats/page.tsx              ← entrypoint
├── Hero (6 KpiTile, 24h live banner, MiniSparkline)
├── AI summary + Activity feed              ← Step 9
│   ├── components/viz/ThreatAISummary.tsx
│   └── components/viz/ThreatActivityFeed.tsx
├── ThreatPipeline                          ← Step 2
│   └── components/viz/ThreatPipeline.tsx
├── FilterBar                               ← Step 3
│   ├── components/ui/MultiSelectPopover.tsx
│   ├── components/ui/DateRangePopover.tsx
│   └── components/ui/SavedQueriesDropdown.tsx
├── BulkToolbar (sticky)                    ← Step 4
└── ThreatList → row click → ThreatDetailDrawer   ← Step 5
    └── 3 action buttons:
        ├── 调取证据链  → ThreatEvidenceModal     ← Step 6
        ├── 转法务起草  → ThreatToLegalModal      ← Step 7
        └── 升级 / 关联 → ThreatPromoteModal      ← Step 8

Shared building blocks (Step 0): ModalShell, KpiTile, DetailRow, FormGroup, TimelineRow, MiniSparkline. All consume the 7-tone palette in lib/tone.ts (info / warning / review / accent / ok / complete / danger).


5. Verification

Static (CI-ready):

go build ./...                # backend
go vet  ./...                 # static analysis
go test ./internal/handler/ -run TestPortalRoutesRegister   # route panic check
(cd frontend && npx tsc --noEmit)
(cd frontend && npx eslint 'src/**/*.{ts,tsx}')
(cd frontend && npm run build)   # 28 static pages

End-to-end smoke against the live deployment (ai-astraweb · portal.astraeye.ai):

TOKEN=...
BASE=https://portal.astraeye.ai/api/v1

curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats/stats" | jq .
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats/activity?limit=10" | jq '.items | length'
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats/ai-summary" | jq '.headline'

THREAT=$(curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats?page_size=1" | jq -r '.items[0].id')

curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats/$THREAT/evidence" | jq '.total'
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/threats/$THREAT/promote-candidates" | jq '.items | length'

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"from_threat_id\":\"$THREAT\",\"document_type\":\"dmca_takedown\",\"language\":\"zh\"}" \
  "$BASE/legal-docs/draft" | jq '.doc.id'

6. Known limits & next iterations

  • AI summary is templated — real LLM swap is a one-function rewrite (BuildThreatAISummary in internal/repository/portal_repo.go).
  • Evidence picker in legal modal records selected IDs but doesn't yet splice individual evidence references into the letter body.
  • Promote → actor mode marks the threat's metadata but doesn't backfill portal_evidence_chains.actor_id — author-bridge is still the pre-existing soft link.
  • No retry / undo on bulk close beyond the existing restore button.