跳转至

社群情报模块 · Social Intel Module

Status: shipped (Phase 6, Steps 1–10) Frontend route: /social-intel Backend prefix: /api/v1/social-intel

10-step iterative upgrade. Same template as the threats / private-servers modules, but adapted for the "is somebody SELLING here?" lens that makes this module Wemade's core monetisation surface (外挂销售 + 私服推广 都从社群上游)。


1. Module purpose

A single workspace for finding real commerce in noisy social-media streams (闲鱼 / 快手 / B站 / Telegram / QQ / Discord / 抖音 / 微博 / 小红书 / 贴吧 / Reddit / TikTok / YouTube / GitHub / 等):

  • Detect → Triage (真销售 / 疑似 / 假阳性) → Evidence → Action (举报 / 关联 / 转法务)
  • 联系方式抽取 (微信 / QQ / TG / Discord / 电话) + 价格 + URL —— the money slice Wemade actually wants
  • 卖家档案聚合 — 同一作者跨平台的多帖折叠成一卡,凸显累犯
  • 频道订阅自管 — 客户能从 portal 自己加 TG/QQ 群进监控池
  • AI 摘要 + 实时活动 流 for situational awareness
  • Multi-tenant: tenant_admin 只看自己 publication;super_admin 看全局

2. Step ledger

Step Scope Deliverable
1 KPI Hero + 卖家排行 5-tile (真销售 / 疑似 / 待审 / 假阳性 / 本周新增) + TopSellersPanel 跨平台排行
2 详情 Drawer + 联系方式展示 SocialIntelDetailDrawer.tsx — 正文 + 5 类联系方式 chips + 价格 + 链接 + KPI / 元数据 / 关联实体占位
3 手动判定 PATCH /:id/classification(4 桶 real_commerce / suspected / fp / unknown)+ drawer 4 按钮 + 卡片角标
4 AI 初评 + 抽取 si_classifier.go:5 类正则抽取(wechat/qq/tg/discord/phone)+ 4 类价格 + URL + 评分;POST /classify-auto
5 关联展示 (4 桥) GET /:id/links:URL→ps · author→actor · content_hash→同源帖子 · threats.from_social_intel_id
6 一键动作 POST /:id/report + link-actor + link-private-server + 扩 /legal-docs/draft 支持 from_social_intel_id;4 modal
7 频道管理 新表 portal_social_channels + CRUD + 合并到 /sync/social-intel/channels(curated 优先 + legacy fallback);/social-intel/channels 子页
8 活动流 + AI 摘要 GET /activity(5 源)+ GET /ai-summary(stats-derived/v1)
9 工作集 tab + 卖家聚合 ?classification=csv + ?trust_min=N + ?sort= + ?group_by_author=true;3 tab + 信度排序 + AuthorGroupCard
10 文档 + e2e + 部署 本文档 + portal routes test + 部署 ai-astraweb + 同步 docs.astraeye.ai

3. Classification taxonomy

4 buckets with real_commerce taking the place of confirmed from the PS module, to emphasise "is somebody SELLING here":

Bucket Meaning Tone
real_commerce 真销售线索 — 有联系方式 + 价格 / 销售关键词 danger
suspected 疑似 — 有 1-2 个商业信号但不充分 warning
unknown 待审 — 默认值,等人审 info
false_positive 假阳性 — 粉丝评论 / 官方账号 / 新闻 ok

Drives all of these views via a single SQL fragment:

COALESCE(
  metadata->>'classification',         -- manual (Step 3)
  metadata->>'classification_hint',    -- auto rule (Step 4)
  'unknown'
)

Manual wins auto. classify-auto defaults to only_unclassified=true so re-runs never clobber human decisions.


4. AI 初评 rule weights (Step 4)

internal/repository/si_classifier.go. Pure function: ClassifySocialIntel(*PortalSocialIntel) → {score, hint, reason, contacts, prices, urls}.

Signal Weight
Content matches a commerce keyword (卖 / 出售 / 包月 / 加微 / vx / 联系 / 价格 / 首充 / 支付宝 / btc / usdt / ...) +25
Any contact extracted (wechat / qq / tg / discord / phone) +35
Any price string extracted (¥X · X元/月 · 包月X · X usdt) +20
matched_keyword class ∈ +10
Content contains at least one URL +5
Short content (<15 chars) -20
Channel / content contains official marker (官方 / 客服 / 官号 / ...) -25

Buckets after capping to [0, 100]: ≥75 real_commerce · 50-74 suspected · 30-49 unknown · <30 false_positive.

Extraction regexes (compiled once, used per-row):

  • WeChat: (?i)(?:微信号?|vx|v信|wx|\+v)[\s::\-]*([a-zA-Z][a-zA-Z0-9_\-]{4,29})
  • QQ: (?i)(?:qq|扣扣|刷q|企鹅)[\s::\-群号]*\s*(\d{5,12})
  • Telegram: (?i)(?:tg|telegram|电报|纸飞机)[\s::@\-]+([a-zA-Z][a-zA-Z0-9_]{4,31})
  • Discord: (?i)discord(?:\.gg|\.com/invite)/([a-zA-Z0-9]+)
  • Phone (CN mobile): \b1[3-9]\d{9}\b with boundary protection
  • Price: (?:¥|¥|RMB)\s*N · N元 · (首充|包月|周卡)X · N USDT/BTC/ETH
  • URL: https?://... or www\...., stop at CJK / whitespace / quotes

Unit-tested in si_classifier_test.go — 7 bucket cases + 1 multi-contact smoke. Tweak weights → re-run go test ./internal/repository/ -run TestClassifySocialIntel.


5. API reference

List & filter

GET /api/v1/social-intel

Query Type Notes
platform string exact match
game string exact match against game_name
keyword string exact match against matched_keyword
search string ILIKE across content_preview / platform_group_name / author
sort enum posted_at_desc (default) | posted_at_asc | trust_desc | trust_asc
classification csv real_commerce,suspected,false_positive,unknown
trust_min int filter rows with trust_score >= N (0 = no filter)
group_by_author bool true → returns SIAuthorGroup rows (one row per author) instead of single posts
page / page_size int default 1 / 30

When group_by_author=true, response shape: { grouped: true, items: SIAuthorGroup[], total, page, page_size }.

Stats

GET /api/v1/social-intel/stats

{
  "total": 850,
  "new_this_week": 47,
  "by_platform": [{"platform":"telegram","count":312}, ...],
  "by_classification": [
    {"classification":"real_commerce","count":24},
    {"classification":"suspected","count":52},
    {"classification":"unknown","count":511},
    {"classification":"false_positive","count":263}
  ],
  "top_keywords": [{"keyword":"CHEAT_TOOL","count":134}, ...],
  "top_groups": [{"platform_group_name":"mir4_global","platform":"telegram","count":42}, ...],
  "top_authors": [
    {"author":"cheatking_007","count":18,"platforms":4},
    ...
  ]
}

Detail

  • GET /:id — full row including metadata blob
  • GET /:id/links{private_servers, actors, same_postings, threats} resolved via the 4 bridges (see §6)

Classification

  • PATCH /:id/classification{ state, reason }; updates metadata.classification / classified_at / classified_by
  • POST /classify-auto?platform=&only_unclassified=true|false (super_admin only) — batch rule engine

Actions (Step 6)

  • POST /:id/report{ channel, channel_url, notes }; appends to metadata.reports[]
  • POST /:id/link-actor{ actor_id } or { new_actor_name }; bidirectional jsonb
  • POST /:id/link-private-server{ private_server_id }; bidirectional jsonb
  • POST /legal-docs/draft extended with from_social_intel_id

Channels (Step 7)

  • GET /social-intel/channels?platform=&status=
  • POST /social-intel/channels{ platform, channel_id, channel_name?, game_name?, notes? } (idempotent on (tenant, platform, channel_id) — re-add = re-activate)
  • PATCH /social-intel/channels/:id{ status?, channel_name?, game_name?, notes? }
  • DELETE /social-intel/channels/:id
  • GET /sync/social-intel/channels (sync key) — merged worker view (curated + legacy fallback)

Activity + AI summary (Step 8)

  • GET /social-intel/activity?limit=30 — 5 sources merged DESC
  • GET /social-intel/ai-summarystats-derived/v1 templated payload

6. Cross-entity bridges

Step 5's GET /:id/links resolves 4 bridges. No FK columns between social_intel and the other entities — all linkage rides on jsonb metadata.

From → To Direction Bridge Written by
si → private_server forward mentioned URL → extracted host → ILIKE ps.domain Step 4 extractor (URL) + Step 5 lookup
si → private_server forward (manual) si.metadata.linked_private_server_ids[]ps.metadata.linked_social_intel_ids[] Step 6 link-private-server
si → actor forward (auto) LOWER(author) = LOWER(actor.name) Step 5 lookup
si → actor forward (manual) si.metadata.linked_actor_idactor.metadata.linked_social_intel_ids[] Step 6 link-actor
si → si sibling same content_hash (cross-platform reposts) sync pipeline + Step 5 lookup
si → threat forward threat.metadata.from_social_intel_id = si.id future LLM promote pipeline
si → legal_doc forward si.metadata.legal_draft_id + legal_drafted_at Step 6 legal draft

7. Front-end component tree

frontend/src/app/(portal)/social-intel/
├── page.tsx
│   ├── KpiTile × 5                                 (Hero classification strip)
│   ├── TopSellersPanel                             (Step 1 — Top 10 卖家)
│   ├── SocialIntelAISummary.tsx                    (Step 8)
│   ├── SocialIntelActivityFeed.tsx                 (Step 8)
│   ├── Workspace tabs (Step 9 — 工作集 / 假阳性 / 全部)
│   ├── 按卖家聚合 toggle + sort (Step 9)
│   ├── IntelCard × N                               (normal mode)
│   └── AuthorGroupCard × N                         (grouped mode)
└── channels/
    └── page.tsx                                    (Step 7 — CRUD channel subscriptions)

frontend/src/components/viz/
├── SocialIntelDetailDrawer.tsx                     (Step 2 — opens on card click)
│   ├── Top badges + content body
│   ├── Contact rows × 5 (Step 2 + 4)
│   ├── Price chips
│   ├── Mentioned URLs
│   ├── AI 初评 panel (Step 4 trust + hint vs current + reason)
│   ├── LinkedEntities tabs (Step 5 — ps / actor / 同源 / threats)
│   └── Footer
│       ├── Classification toolbar (Step 3 — 4 buttons)
│       └── 4 action buttons → SocialIntelReport / LinkActor / LinkPS / Legal modals (Step 6)
├── SocialIntelReportModal.tsx
├── SocialIntelLinkActorModal.tsx
├── SocialIntelLinkPSModal.tsx
└── SocialIntelLegalModal.tsx

8. Verification

Local:

# Compile + vet + tests
go build ./...
go vet  ./...
go test ./internal/handler/    -run TestPortalRoutesRegister   # all 28 routes
go test ./internal/repository/ -run TestClassifySocialIntel    # rule-engine bands

# Frontend
cd frontend
npx tsc --noEmit
npx eslint src/app/\(portal\)/social-intel src/components/viz/SocialIntel*.tsx
npm run build       # 29 static pages, /social-intel + /social-intel/channels

End-to-end smoke against production (ai-astraweb · portal.astraeye.ai):

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

# Hero KPIs
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/social-intel/stats" \
  | jq '{total, new_this_week, by_classification, top_authors: (.top_authors|length)}'

# Workspace tab
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/social-intel?classification=real_commerce,suspected,unknown&page_size=1" \
  | jq '.total'

# Author-grouped view
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/social-intel?group_by_author=true&page_size=5" \
  | jq '.items | map({author, post_count, platform_count})'

# AI 初评 batch (super_admin)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "$BASE/social-intel/classify-auto?only_unclassified=true" \
  | jq '{processed, by_hint, contacts_total, prices_total, urls_total}'

# Activity + AI summary
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/social-intel/activity?limit=10" \
  | jq '.items | length'
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/social-intel/ai-summary" \
  | jq '.headline'

# Channel CRUD
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/social-intel/channels" | jq '.count'

9. Known limits / next steps

  • Listener stack not yet redeployed. The portal accepts new channel subscriptions, but the QQ / TG listeners that consume them live on ai-astrahost (or wherever discovery lands). After this rollout, the workers should be pointed at the new merge endpoint.
  • AI summary is templated (stats-derived/v1). Replace BuildSocialIntelAISummary body to swap in a real LLM; the payload shape and frontend stay.
  • Author-actor bridge is name-match only. Two sellers using identical handles would conflate; mitigation is Step 6's manual link-actor modal which uses an explicit actor_id.
  • No "evidence creation" from drawer. Mirror of the open work in the PS module — evidence chain creation is a separate iteration.
  • from_social_intel_id on threats not yet populated. The 4th bridge (Step 5) reads this field but the LLM-promote pipeline that would set it isn't wired in this phase.