社群情报模块 · Social Intel Module¶
Status: shipped (Phase 6, Steps 1–10) Frontend route:
/social-intelBackend 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}\bwith boundary protection - Price:
(?:¥|¥|RMB)\s*N·N元·(首充|包月|周卡)X·N USDT/BTC/ETH - URL:
https?://...orwww\...., 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 blobGET /:id/links—{private_servers, actors, same_postings, threats}resolved via the 4 bridges (see §6)
Classification¶
PATCH /:id/classification—{ state, reason }; updatesmetadata.classification / classified_at / classified_byPOST /classify-auto?platform=&only_unclassified=true|false(super_admin only) — batch rule engine
Actions (Step 6)¶
POST /:id/report—{ channel, channel_url, notes }; appends tometadata.reports[]POST /:id/link-actor—{ actor_id }or{ new_actor_name }; bidirectional jsonbPOST /:id/link-private-server—{ private_server_id }; bidirectional jsonbPOST /legal-docs/draftextended withfrom_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/:idGET /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 DESCGET /social-intel/ai-summary—stats-derived/v1templated 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_id ↔ actor.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). ReplaceBuildSocialIntelAISummarybody 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-actormodal 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_idon 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.