Publish updated WeChat Radar

This commit is contained in:
joeseesun
2026-05-26 09:10:04 +08:00
parent 8abb29e13e
commit de7ec99fed
44 changed files with 2900 additions and 746 deletions
+68 -32
View File
@@ -98,6 +98,7 @@ export interface DashboardLinkHighlight {
title: string;
url: string;
domain: string;
source: string;
score: number;
verdict: string;
count: number;
@@ -149,7 +150,7 @@ export function buildDashboardIntelligence(
groupNames = new Map<string, string>(),
): DashboardIntelligence {
date = resolveIntelligenceDate(date);
const key = `dashboard-intelligence:${date}:v11`;
const key = `dashboard-intelligence:${date}:v14`;
const cached = cache.get(key) as DashboardIntelligence | undefined;
if (cached) return cached;
@@ -174,6 +175,37 @@ export function buildDashboardIntelligence(
LIMIT ?`,
)
.all(minusDays(date, 7), date, 9000) as MessageSignalRow[];
const linkRows = db()
.prepare(
`SELECT
ml.url,
ml.canonical_url,
ml.title,
ml.domain,
ml.source,
ml.confidence,
ml.time,
ml.chatroom_id,
m.content
FROM message_links ml
JOIN messages m
ON m.chatroom_id = ml.chatroom_id
AND m.local_id = ml.local_id
WHERE ml.date = ?
ORDER BY ml.timestamp DESC
LIMIT 600`,
)
.all(date) as Array<{
url: string;
canonical_url: string;
title: string | null;
domain: string;
source: string;
confidence: number;
time: string;
chatroom_id: string;
content: string;
}>;
const candidates: DashboardSignalItem[] = [];
const opportunities: DashboardOpportunityItem[] = [];
@@ -203,6 +235,7 @@ export function buildDashboardIntelligence(
groups: Set<string>;
last_seen: string;
snippets: string[];
sources: Set<string>;
}
>();
@@ -261,28 +294,30 @@ export function buildDashboardIntelligence(
if (TOOL_RE.test(clean)) source.tool_count++;
sourceMap.set(sourceKey, source);
for (const url of extractUrls(row.content)) {
const domain = domainOf(url);
if (!domain) continue;
const kind = isArticleUrl(url) ? 'article' : isToolUrl(url, clean) ? 'tool' : null;
if (!kind) continue;
const key = normalizeUrlKey(url);
const bucket = linkBuckets.get(key) ?? {
kind,
title: titleFromLinkContext(row.content, url),
url,
domain,
count: 0,
groups: new Set<string>(),
last_seen: row.time,
snippets: [],
};
bucket.count++;
bucket.groups.add(row.chatroom_id);
bucket.last_seen = bucket.last_seen > row.time ? bucket.last_seen : row.time;
if (clean) bucket.snippets.push(clean.slice(0, 80));
linkBuckets.set(key, bucket);
}
}
for (const row of linkRows) {
const kind = isArticleUrl(row.canonical_url) ? 'article' : isToolUrl(row.canonical_url, row.content) ? 'tool' : null;
if (!kind) continue;
const key = normalizeUrlKey(row.canonical_url);
const clean = cleanContent(row.content);
const bucket = linkBuckets.get(key) ?? {
kind,
title: (row.title || titleFromLinkContext(row.content, row.url)).slice(0, 80),
url: row.url,
domain: row.domain || domainOf(row.canonical_url),
count: 0,
groups: new Set<string>(),
last_seen: row.time,
snippets: [],
sources: new Set<string>(),
};
bucket.count++;
bucket.groups.add(row.chatroom_id);
bucket.sources.add(row.source);
bucket.last_seen = bucket.last_seen > row.time ? bucket.last_seen : row.time;
if (clean) bucket.snippets.push(clean.slice(0, 80));
linkBuckets.set(key, bucket);
}
const mustRead = candidates
@@ -430,6 +465,7 @@ function buildLinkHighlights(
groups: Set<string>;
last_seen: string;
snippets: string[];
sources: Set<string>;
}
>,
): DashboardLinkHighlight[] {
@@ -441,6 +477,7 @@ function buildLinkHighlights(
title: item.title,
url: item.url,
domain: item.domain,
source: preferredLinkSource(item.sources),
score,
verdict: verdictForLink(item.kind, item.count, item.groups.size, item.snippets.join(' ')),
count: item.count,
@@ -452,6 +489,13 @@ function buildLinkHighlights(
.slice(0, MAX_LINK_HIGHLIGHTS);
}
function preferredLinkSource(sources: Set<string>): string {
if (sources.has('wechat_raw')) return 'wechat_raw';
if (sources.has('public_search')) return 'public_search';
if (sources.has('manual')) return 'manual';
return Array.from(sources)[0] ?? 'plain_url';
}
function buildPeopleRadar(
sourceMap: Map<
string,
@@ -602,14 +646,6 @@ function minusDays(date: string, days: number): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function extractUrls(content: string): string[] {
const decoded = decodeHtml(content);
return Array.from(decoded.matchAll(URL_GLOBAL_RE))
.map((m) => cleanUrl(m[0]))
.filter(Boolean)
.slice(0, 6);
}
function cleanUrl(raw: string): string {
return raw
.replace(/[),,。;;!?!?、\]}>]+$/g, '')
@@ -645,7 +681,7 @@ function isArticleUrl(raw: string): boolean {
if (host === 'mp.weixin.qq.com') return true;
if ((host === 'x.com' || host === 'twitter.com') && /\/status\/\d{12,}/.test(u.pathname)) return true;
if (host === 'youtube.com' || host === 'youtu.be') return true;
return /zhihu|toutiao|sohu|163\.com|qq\.com|medium\.com|substack\.com|juejin\.cn/i.test(host);
return /zhihu|toutiao|sohu|163\.com|qq\.com|medium\.com|substack\.com|juejin\.cn|podcasts\.apple\.com|podscan\.fm/i.test(host);
} catch {
return false;
}