feat(news-morning): add full-tier channels with cross-group dedupe
Expand daily briefing prefetch and template to cover Asia-Pacific, commodities, AI/chips, policy, community, science, culture, startup, crypto, geopolitics, cities and wire feeds, with URL/title deduplication across sections. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Enable global-source news morning template and trigger prod worker regenerate + push.
|
||||
* Enable full-source news morning template and trigger prod worker regenerate + push.
|
||||
*/
|
||||
import process from 'node:process';
|
||||
import mysql from 'mysql2/promise';
|
||||
@@ -29,13 +29,12 @@ async function main() {
|
||||
enabled: current.enabled !== false,
|
||||
forceGenerateOnce: true,
|
||||
taskSpecAppend: [
|
||||
'【全球扩展栏目 · 必须生成】',
|
||||
'除原有栏目外,必须新增并写满以下 4 个 section(quick-link + div.section):',
|
||||
'1. id=google:「🔎 Google 热点 · Global Headlines」2~3 条,标题可英文,正文中文摘要,来源 Reuters/BBC/AP/Google News 等;',
|
||||
'2. id=x:「𝕏 X 平台热点 · Trending」2~3 条,X/Twitter 热议或媒体转述,标题可中英混合;',
|
||||
'3. id=universities:「🎓 全球高校 · Research & Campus」2~3 条,MIT/Stanford/Oxford 等科研或校园新闻,标题可英文;',
|
||||
'4. id=global_markets:「📈 全球股市 · Markets」2~3 条,美股/欧股/日股/港股等,可含 Fed/Nasdaq/FTSE 等英文术语;',
|
||||
'英文栏目标题可英文或中英并列,每条必须有中文摘要与来源 URL;禁止整段纯英文正文。',
|
||||
'【全渠道扩展 · 必须生成且全页去重】',
|
||||
'除原有栏目外,必须新增并写满以下 section(quick-link + div.section),每条 2 card(crypto 有则 1~2,无则可省略 section):',
|
||||
'asia_pacific / commodities / ai_chips / policy / community / science / culture / startup / crypto / geopolitics / local_life / official_feeds,',
|
||||
'以及 google / x / universities / global_markets。',
|
||||
'全页去重:同一 URL 或同一核心事件只允许出现一次;主栏目优先,扩展栏目必须换角度或跳过。',
|
||||
'英文栏目标题可英文,每条必须有中文摘要与来源 URL;禁止整段纯英文正文。',
|
||||
].join('\n'),
|
||||
};
|
||||
const now = Date.now();
|
||||
@@ -46,13 +45,13 @@ async function main() {
|
||||
config_json = VALUES(config_json),
|
||||
updated_by = VALUES(updated_by),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[CONFIG_KEY, JSON.stringify(next), 'global-source-regenerate', now],
|
||||
[CONFIG_KEY, JSON.stringify(next), 'full-source-regenerate', now],
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
forceGenerateOnce: true,
|
||||
taskSpecAppendChars: next.taskSpecAppend.length,
|
||||
templateNote: 'Worker on 143 must run code with taskSpecAppend + new search queries for full effect.',
|
||||
templateNote: 'Worker must run code with full template + dedupe prefetch for best effect.',
|
||||
}, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,21 @@ const SECTION_LABELS = {
|
||||
world: 'WORLD',
|
||||
domestic: 'CHINA',
|
||||
google: 'GLOBAL',
|
||||
x: 'SOCIAL',
|
||||
asia_pacific: 'ASIA',
|
||||
commodities: 'ENERGY',
|
||||
ai_chips: 'AI',
|
||||
policy: 'POLICY',
|
||||
universities: 'CAMPUS',
|
||||
global_markets: 'MARKETS',
|
||||
community: 'COMMUNITY',
|
||||
science: 'SCIENCE',
|
||||
culture: 'CULTURE',
|
||||
startup: 'STARTUP',
|
||||
crypto: 'CRYPTO',
|
||||
geopolitics: 'GEOPOL',
|
||||
local_life: 'CITIES',
|
||||
official_feeds: 'WIRE',
|
||||
finance: 'FINANCE',
|
||||
tech: 'TECH',
|
||||
technews: 'TECH',
|
||||
@@ -547,16 +562,31 @@ function renderGenericSection(
|
||||
}
|
||||
|
||||
const DEFAULT_SECTION_CARD_LIMITS = {
|
||||
headlines: 7,
|
||||
world: 5,
|
||||
domestic: 5,
|
||||
google: 3,
|
||||
finance: 5,
|
||||
tech: 5,
|
||||
sports: 3,
|
||||
headlines: 6,
|
||||
world: 4,
|
||||
domestic: 4,
|
||||
google: 2,
|
||||
x: 2,
|
||||
asia_pacific: 2,
|
||||
commodities: 2,
|
||||
ai_chips: 2,
|
||||
policy: 2,
|
||||
universities: 2,
|
||||
global_markets: 2,
|
||||
community: 2,
|
||||
science: 2,
|
||||
culture: 2,
|
||||
startup: 2,
|
||||
crypto: 2,
|
||||
geopolitics: 2,
|
||||
local_life: 2,
|
||||
official_feeds: 2,
|
||||
finance: 4,
|
||||
tech: 4,
|
||||
sports: 2,
|
||||
technews: 2,
|
||||
hot: 3,
|
||||
knowledge: 4,
|
||||
hot: 2,
|
||||
knowledge: 3,
|
||||
};
|
||||
|
||||
const DEFAULT_CARD_BODY_CHARS = 150;
|
||||
@@ -593,13 +623,28 @@ const TRIM_PROFILES = CARD_LIMIT_TIERS.map((sectionCardLimits, index) => ({
|
||||
* 知识小卡片、体育、热搜等补充栏目先让位,保证各栏目都还有内容。
|
||||
*/
|
||||
const SECTION_SHRINK_WEIGHTS = {
|
||||
knowledge: 1.6,
|
||||
sports: 1.4,
|
||||
hot: 1.35,
|
||||
google: 1.3,
|
||||
technews: 1.3,
|
||||
knowledge: 1.7,
|
||||
culture: 1.65,
|
||||
community: 1.62,
|
||||
crypto: 1.6,
|
||||
local_life: 1.58,
|
||||
startup: 1.55,
|
||||
science: 1.5,
|
||||
sports: 1.45,
|
||||
hot: 1.4,
|
||||
x: 1.38,
|
||||
google: 1.35,
|
||||
official_feeds: 1.33,
|
||||
asia_pacific: 1.3,
|
||||
commodities: 1.28,
|
||||
universities: 1.25,
|
||||
ai_chips: 1.2,
|
||||
geopolitics: 1.15,
|
||||
policy: 1.1,
|
||||
global_markets: 1.08,
|
||||
technews: 1.05,
|
||||
tech: 1,
|
||||
finance: 1,
|
||||
finance: 0.95,
|
||||
world: 0.9,
|
||||
domestic: 0.9,
|
||||
headlines: 0.65,
|
||||
@@ -879,7 +924,22 @@ function fitInlineContentToBudget(html, bodyBudget, {
|
||||
const fallbackProfile = {
|
||||
...BASE_TRIM_FLAGS,
|
||||
skipKnowledge: true,
|
||||
sectionCardLimits: { headlines: 3, world: 2, domestic: 2, google: 1, finance: 2, tech: 2, sports: 1, technews: 1, hot: 1, knowledge: 0 },
|
||||
sectionCardLimits: {
|
||||
headlines: 3,
|
||||
world: 2,
|
||||
domestic: 2,
|
||||
google: 1,
|
||||
x: 1,
|
||||
asia_pacific: 1,
|
||||
finance: 2,
|
||||
tech: 2,
|
||||
ai_chips: 1,
|
||||
global_markets: 1,
|
||||
sports: 1,
|
||||
technews: 1,
|
||||
hot: 1,
|
||||
knowledge: 0,
|
||||
},
|
||||
maxBodyChars: 80,
|
||||
};
|
||||
const built = render(fallbackProfile);
|
||||
|
||||
@@ -33,7 +33,7 @@ const MAX_THUMB_BYTES = 64 * 1024;
|
||||
export const NEWS_MORNING_TEMPLATE_REFERENCE_URL =
|
||||
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/daily-news-0915.html';
|
||||
|
||||
export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-18-global';
|
||||
export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-18-full';
|
||||
|
||||
/** 生产 daily-news-0915 版式:div.section + 扩展全球栏目。 */
|
||||
export const NEWS_MORNING_TEMPLATE_0915_SPEC = [
|
||||
@@ -41,23 +41,36 @@ export const NEWS_MORNING_TEMPLATE_0915_SPEC = [
|
||||
`参照页:${NEWS_MORNING_TEMPLATE_REFERENCE_URL}`,
|
||||
'写页前可先 read_file 打开同目录最近 daily-news-*.html 或上述 URL 对应文件,对照 CSS 与 DOM 骨架;正文条目必须全部来自当日检索。',
|
||||
'1. 外层:`.wrap` > `.hero`(`.hero-content` 内 h1「📰 每日新闻早报」、`.date-badge`、`.subtitle` 一句主编导语)> `.container`。',
|
||||
'2. quick-links 固定顺序:📰 要闻|🌤️ 天气|🌍 国际|🇨🇳 国内|💰 财经|🤖 科技|🏆 体育|🔥 热搜|🔎 Google|𝕏 X|🎓 高校|📈 全球股市|📚 小知识|📅 历史(href 对应下方 id)。',
|
||||
'2. quick-links 固定顺序:📰 要闻|🌤️ 天气|🌍 国际|🇨🇳 国内|💰 财经|🤖 科技|🏆 体育|🔥 热搜|🔎 Google|𝕏 X|🌏 亚太|⛽ 能源|🧠 AI芯片|⚖️ 政策|🎓 高校|📈 全球股市|💬 社区|🔬 科学|🎬 文娱|🚀 创投|₿ 加密|🛡️ 地缘|🏙️ 城市|📡 权威快讯|📚 小知识|📅 历史(href 对应下方 id)。',
|
||||
'3. 区块容器必须用 `<div class="section" id="...">`(禁止 `<section id>`);`.section-title` 含 `<span class="icon">` emoji。',
|
||||
'4. 区块 id 与内容:',
|
||||
' - id=headlines:「今日要闻 · {完整日期}」;至少 3 条 `.card.highlight-box`(tag + h3 + p + source),标题 15~28 字、前 10 字有钩子。',
|
||||
' - id=weather:「今日天气 · {月日}」;`.weather-today` + `.weather-grid` 8~12 张 `.weather-card`;热门城市(北京、上海、广州、深圳、成都、杭州、西安、重庆、武汉、南京、厦门、三亚等),禁止用区域预报顶替城市卡。',
|
||||
' - id=world / domestic / finance / tech / sports:各 3~4 条 card。',
|
||||
' - id=hot:「今日热门事件与热搜 · {月日}」;2~3 条 highlight-box。',
|
||||
' - id=google:「🔎 Google 热点 · Global Headlines」;2~3 条 card,标题可英文,p 用中文摘要,source 链到 Reuters/BBC/AP/Google News 等。',
|
||||
' - id=x:「𝕏 X 平台热点 · Trending」;2~3 条 card,X/Twitter 热议或媒体转述,标题可中英混合。',
|
||||
' - id=universities:「🎓 全球高校 · Research & Campus」;2~3 条 card,MIT/Stanford/Oxford/Cambridge 等高校新闻,标题可英文。',
|
||||
' - id=global_markets:「📈 全球股市 · Markets」;2~3 条 card,美股/欧股/日股/港股/A50 等,可含 Fed/Nasdaq/FTSE 等英文术语。',
|
||||
' - id=google:「🔎 Google 热点 · Global Headlines」;2 条 card,标题可英文,p 用中文摘要。',
|
||||
' - id=x:「𝕏 X 平台热点 · Trending」;2 条 card,X/Twitter 热议或媒体转述。',
|
||||
' - id=asia_pacific:「🌏 亚太观察 · Asia-Pacific」;2 条 card,日韩台港/东盟视角。',
|
||||
' - id=commodities:「⛽ 能源大宗 · Commodities」;2 条 card,原油/黄金/铜/OPEC 等。',
|
||||
' - id=ai_chips:「🧠 AI & 芯片 · Tech Deep」;2 条 card,大模型/半导体/NVIDIA/出口管制等。',
|
||||
' - id=policy:「⚖️ 政策监管 · Policy Watch」;2 条 card,中外监管与重大政策。',
|
||||
' - id=universities:「🎓 全球高校 · Research & Campus」;2 条 card,高校科研/校园新闻,标题可英文。',
|
||||
' - id=global_markets:「📈 全球股市 · Markets」;2 条 card,美股/欧股/日股/港股等。',
|
||||
' - id=community:「💬 社区热议 · Reddit/HN」;2 条 card,Reddit/Hacker News 热议经媒体转述。',
|
||||
' - id=science:「🔬 科学前沿 · Science」;2 条 card,科研/太空/医学突破。',
|
||||
' - id=culture:「🎬 文娱动态 · Culture」;2 条 card,影视/音乐/游戏行业新闻(禁八卦绯闻)。',
|
||||
' - id=startup:「🚀 创投速递 · Startup」;2 条 card,融资/IPO/并购。',
|
||||
' - id=crypto:「₿ 加密货币 · Crypto」;0~2 条 card,仅当 BTC/ETH/监管有重大波动时写,否则可省略本 section。',
|
||||
' - id=geopolitics:「🛡️ 地缘军事 · Geopolitics」;2 条 card,冲突/制裁/外交/防务(与 id=world 去重)。',
|
||||
' - id=local_life:「🏙️ 城市动态 · Cities」;2 条 card,北上广深成渝杭西等主要城市民生/交通/政策。',
|
||||
' - id=official_feeds:「📡 权威快讯 · Wire Services」;2 条 card,Reuters/BBC/AP/NPR 等通讯社快讯,标题可英文。',
|
||||
' - id=knowledge:「📚 知识小卡片」;1~2 条短知识 card。',
|
||||
' - id=history:「📅 近7天新闻早报」;列出近 7 天 daily-news-MMDD.html 链接。',
|
||||
'5. 每条 card:span.tag + h3 + p(2~4 句)+ div.source。',
|
||||
'6. footer:新闻来源列表 + 整理时间 + TKMind 品牌;meta mindspace-cover、title、description;生成 daily-news-MMDD.thumbnail.png。',
|
||||
'7. 推送微信草稿时转为内联 style 版式(微信不支持 <style> 标签),禁止整页长图;底部追加统一「阅读原文 + 二维码」layout;公众号侧 history/knowledge 可略短。',
|
||||
'8. 英文栏目允许英文标题或中英并列,但每条必须有中文可读摘要,禁止整段纯英文正文。',
|
||||
'9. 全页去重(必须):同一 URL、同一核心事件只允许出现一次;若已在 headlines/world/domestic/finance/tech 出现,扩展栏目不得重复同角度,可换角度或跳过;写页前合并预取+tkmind_search+web_search 后再去重。',
|
||||
].join('\n');
|
||||
|
||||
/** 兼容旧引用名 */
|
||||
@@ -67,13 +80,14 @@ export const NEWS_MORNING_TEMPLATE_0910_SPEC = NEWS_MORNING_TEMPLATE_0915_SPEC;
|
||||
export const NEWS_MORNING_FRESH_CONTENT_SPEC = [
|
||||
'内容硬约束(必须遵守,优先于版式):',
|
||||
'1. 检索必须双路:先 load_skill → web,再同一轮调用 tkmind_search(type=news) 与 web_search,合并去重后写页;禁止只靠 web_search。',
|
||||
'2. 覆盖面至少包括:国内、国际、热搜、财经、科技、体育、天气,以及 Google / X / 全球高校 / 全球股市四个英文栏目;不得凭记忆编造。',
|
||||
'2. 覆盖面至少包括:国内、国际、热搜、财经、科技、体育、天气,以及 Google/X/亚太/能源/AI芯片/政策/高校/全球股市/社区/科学/文娱/创投/加密(有则写)/地缘/城市/权威快讯等扩展栏目;不得凭记忆编造。',
|
||||
'3. 若附有「专用联网搜索预取」,必须优先从中选题并写入对应栏目;预取有结果时禁止输出「未能获取」类空页。',
|
||||
'4. 若 tkmind_search / web_search 暂时不可用,但预取 brief 已有条目:仍必须用预取结果写满各栏目,并在 source 标注预取来源 URL。',
|
||||
'5. 仅当预取 brief 各路均为空且双路搜索均失败时,才允许写「检索暂不可用」说明页;禁止用历史 daily-news 正文充数。',
|
||||
'6. 标题规则:中文标题 15~28 字;英文栏目标题可英文或中英并列;每条必须可核验并写来源 URL。',
|
||||
'7. Google / X / 全球高校 / 全球股市:优先用预取 brief 对应分组,可保留英文标题,正文用 2~4 句中文摘要。',
|
||||
'8. 禁止复制、改写历史 daily-news-*.html 的正文;旧稿仅作 HTML 结构参考。',
|
||||
'7. 扩展英文栏目:优先用预取 brief 对应分组,可保留英文标题,正文用 2~4 句中文摘要。',
|
||||
'8. 全页去重:同一事件(如「美联储加息」「Fed rate hike」)只保留一条;主栏目优先,扩展栏目必须不同角度或跳过;禁止复制粘贴同段摘要。',
|
||||
'9. 禁止复制、改写历史 daily-news-*.html 的正文;旧稿仅作 HTML 结构参考。',
|
||||
].join('\n');
|
||||
|
||||
function normalizeBoolean(value, fallback = false) {
|
||||
@@ -129,7 +143,7 @@ function formatLocalDateParts(date = new Date(), timezone = 'Asia/Shanghai') {
|
||||
}
|
||||
|
||||
/** 检测「检索失败 / 空内容」降级页,需强制重生成。 */
|
||||
export function isNewsMorningContentDegraded(html, { minStoryCards = 6 } = {}) {
|
||||
export function isNewsMorningContentDegraded(html, { minStoryCards = 12 } = {}) {
|
||||
const source = String(html ?? '');
|
||||
if (!source.trim()) return true;
|
||||
if (/关于今日早报的重要说明|未填充任何当日具体新闻条目|未能获取当日可核验新闻/u.test(source)) {
|
||||
|
||||
@@ -44,6 +44,18 @@ export function buildNewsMorningSearchQueries(dateLabel) {
|
||||
{ id: 'x', title: 'X 平台热点', query: `${englishFresh} trending news site:x.com OR site:twitter.com` },
|
||||
{ id: 'universities', title: '全球高校', query: `${englishFresh} university research breakthrough news site:edu OR MIT OR Stanford OR Oxford` },
|
||||
{ id: 'global_markets', title: '全球股市', query: `${englishFresh} global stock market finance wall street nikkei ftse dax` },
|
||||
{ id: 'asia_pacific', title: '亚太观察', query: `${englishFresh} Asia Pacific news Japan Korea Taiwan Hong Kong ASEAN site:scmp.com OR site:asia.nikkei.com` },
|
||||
{ id: 'commodities', title: '能源大宗', query: `${englishFresh} oil price gold crude commodities OPEC energy market ${year}` },
|
||||
{ id: 'ai_chips', title: 'AI 与芯片', query: `${englishFresh} artificial intelligence LLM semiconductor NVIDIA chip export OpenAI ${year}` },
|
||||
{ id: 'policy', title: '政策监管', query: `${fresh} 政策 监管 国务院 央行 证监会 欧盟 FTC SEC 新规` },
|
||||
{ id: 'community', title: '社区热议', query: `${englishFresh} trending news site:reddit.com OR site:news.ycombinator.com` },
|
||||
{ id: 'science', title: '科学前沿', query: `${englishFresh} science breakthrough research Nature NASA space medicine ${year}` },
|
||||
{ id: 'culture', title: '文娱动态', query: `${fresh} 文娱 影视 音乐 游戏 票房 行业 新闻` },
|
||||
{ id: 'startup', title: '创投速递', query: `${englishFresh} startup funding venture capital IPO acquisition tech ${year}` },
|
||||
{ id: 'crypto', title: '加密货币', query: `${englishFresh} bitcoin ethereum crypto regulation market ${year}` },
|
||||
{ id: 'geopolitics', title: '地缘军事', query: `${englishFresh} geopolitics military conflict defense sanctions diplomacy ${year}` },
|
||||
{ id: 'local_life', title: '城市动态', query: `${fresh} 北京 上海 广州 深圳 成都 杭州 西安 城市 交通 政策 民生` },
|
||||
{ id: 'official_feeds', title: '权威快讯', query: `${englishFresh} breaking news site:reuters.com OR site:bbc.com/world OR site:apnews.com OR site:npr.org` },
|
||||
].filter((item) => item.query.trim());
|
||||
}
|
||||
|
||||
@@ -65,18 +77,99 @@ export function filterFreshNewsMorningSearchResults(results = [], { year = null
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeNewsMorningUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url ?? '').trim());
|
||||
parsed.hash = '';
|
||||
parsed.search = '';
|
||||
const host = parsed.hostname.replace(/^www\./i, '').toLowerCase();
|
||||
const pathname = parsed.pathname.replace(/\/+$/, '').toLowerCase();
|
||||
return `${host}${pathname}`;
|
||||
} catch {
|
||||
return String(url ?? '').trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeNewsMorningTitle(title) {
|
||||
return String(title ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isDuplicateNewsMorningStory(existingTitles = [], candidate = {}) {
|
||||
const urlKey = normalizeNewsMorningUrl(candidate?.url);
|
||||
if (urlKey && existingTitles.some((item) => item.urlKey === urlKey)) return true;
|
||||
const title = normalizeNewsMorningTitle(candidate?.title);
|
||||
if (!title) return false;
|
||||
return existingTitles.some((item) => {
|
||||
const other = item.title;
|
||||
if (!other) return false;
|
||||
if (other === title) return true;
|
||||
const shorter = other.length <= title.length ? other : title;
|
||||
const longer = other.length > title.length ? other : title;
|
||||
if (shorter.length >= 8 && longer.includes(shorter)) return true;
|
||||
const wordsA = new Set(shorter.split(' ').filter((word) => word.length >= 3));
|
||||
const wordsB = new Set(longer.split(' ').filter((word) => word.length >= 3));
|
||||
if (wordsA.size < 2 || wordsB.size < 2) return false;
|
||||
let overlap = 0;
|
||||
for (const word of wordsA) {
|
||||
if (wordsB.has(word)) overlap += 1;
|
||||
}
|
||||
const minSize = Math.min(wordsA.size, wordsB.size);
|
||||
return overlap >= 3 && overlap / minSize >= 0.6;
|
||||
});
|
||||
}
|
||||
|
||||
/** 按栏目优先级跨组去重:先出现的栏目保留,后续栏目跳过重复 URL/标题。 */
|
||||
export function dedupeNewsMorningSearchGroups(groups = [], { seen = null } = {}) {
|
||||
const seenUrls = seen?.urls ?? new Set();
|
||||
const seenTitles = seen?.titles ?? [];
|
||||
const dedupedGroups = [];
|
||||
let removedCount = 0;
|
||||
|
||||
for (const group of Array.isArray(groups) ? groups : []) {
|
||||
const results = [];
|
||||
for (const item of Array.isArray(group?.results) ? group.results : []) {
|
||||
const urlKey = normalizeNewsMorningUrl(item?.url);
|
||||
if (urlKey && seenUrls.has(urlKey)) {
|
||||
removedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (isDuplicateNewsMorningStory(seenTitles, item)) {
|
||||
removedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (urlKey) seenUrls.add(urlKey);
|
||||
seenTitles.push({ urlKey, title: normalizeNewsMorningTitle(item?.title) });
|
||||
results.push(item);
|
||||
}
|
||||
dedupedGroups.push({ ...group, results });
|
||||
}
|
||||
|
||||
return {
|
||||
groups: dedupedGroups,
|
||||
removedCount,
|
||||
seen: { urls: seenUrls, titles: seenTitles },
|
||||
};
|
||||
}
|
||||
|
||||
function truncateSnippet(value, max = 90) {
|
||||
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return '';
|
||||
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
||||
}
|
||||
|
||||
export function formatNewsMorningSearchBrief(groups = [], { dateLabel = null } = {}) {
|
||||
export function formatNewsMorningSearchBrief(groups = [], { dateLabel = null, dedupeRemovedCount = 0 } = {}) {
|
||||
const lines = [
|
||||
'【专用联网搜索预取】以下结果来自专用新闻检索(与 web_search 并列,不是替代)。',
|
||||
'写页面时必须:1)优先从下列结果覆盖头条/国内/国际/财经/科技/体育/热搜,以及 Google 热点、X 平台、全球高校、全球股市四个英文栏目;2)同一轮再调用 tkmind_search(type=news) 与 web_search 补漏并核对来源;3)禁止只使用 web_search;4)英文栏目可保留英文标题,正文用中文摘要并附原文链接。',
|
||||
'写页面时必须:1)优先从下列结果覆盖各栏目;2)同一轮再调用 tkmind_search(type=news) 与 web_search 补漏并核对来源;3)禁止只使用 web_search;4)全页去重:同一 URL/同一事件只允许出现一次,优先保留要闻/国内/国际/财经/科技,扩展栏目用不同角度或跳过;5)英文栏目标题可英文,正文用中文摘要并附原文链接。',
|
||||
];
|
||||
if (dateLabel?.iso) lines.push(`预取日期:${dateLabel.iso}`);
|
||||
if (dedupeRemovedCount > 0) {
|
||||
lines.push(`预取跨栏目去重:已剔除 ${dedupeRemovedCount} 条重复 URL/标题,写页时仍须对双路搜索结果再次去重。`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
let hitCount = 0;
|
||||
@@ -100,7 +193,7 @@ export function formatNewsMorningSearchBrief(groups = [], { dateLabel = null } =
|
||||
}
|
||||
|
||||
if (!hitCount) {
|
||||
lines.push('预取没有返回条目。仍必须调用 tkmind_search(type=news) 与 web_search,覆盖国内、国际、财经、科技、体育、热搜后再写页面。');
|
||||
lines.push('预取没有返回条目。仍必须调用 tkmind_search(type=news) 与 web_search,覆盖各栏目后再写页面。');
|
||||
}
|
||||
return lines.join('\n').trim();
|
||||
}
|
||||
@@ -132,7 +225,7 @@ export async function prefetchNewsMorningSearxngBrief({
|
||||
timezone = 'Asia/Shanghai',
|
||||
env = process.env,
|
||||
searchImpl = searchSearxng,
|
||||
limit = 6,
|
||||
limit = 5,
|
||||
} = {}) {
|
||||
const dateLabel = formatLocalDateParts(now, timezone);
|
||||
const queries = buildNewsMorningSearchQueries(dateLabel);
|
||||
@@ -152,7 +245,7 @@ export async function prefetchNewsMorningSearxngBrief({
|
||||
const raw = await searchNewsThenGeneral(item.query, {
|
||||
endpoint,
|
||||
searchImpl,
|
||||
limit: limit + 4,
|
||||
limit: limit + 3,
|
||||
timeoutMs,
|
||||
});
|
||||
const results = filterFreshNewsMorningSearchResults(
|
||||
@@ -161,5 +254,6 @@ export async function prefetchNewsMorningSearxngBrief({
|
||||
).slice(0, limit);
|
||||
return { ...item, results };
|
||||
}));
|
||||
return formatNewsMorningSearchBrief(groups, { dateLabel });
|
||||
const { groups: dedupedGroups, removedCount } = dedupeNewsMorningSearchGroups(groups);
|
||||
return formatNewsMorningSearchBrief(dedupedGroups, { dateLabel, dedupeRemovedCount: removedCount });
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildNewsMorningSearchQueries,
|
||||
dedupeNewsMorningSearchGroups,
|
||||
filterFreshNewsMorningSearchResults,
|
||||
formatNewsMorningSearchBrief,
|
||||
isDuplicateNewsMorningStory,
|
||||
prefetchNewsMorningSearxngBrief,
|
||||
} from './wechat-news-morning-search.mjs';
|
||||
|
||||
test('buildNewsMorningSearchQueries covers lead, domestic, world, finance, tech, sports and hot', () => {
|
||||
test('buildNewsMorningSearchQueries covers core and extended channels', () => {
|
||||
const queries = buildNewsMorningSearchQueries({
|
||||
iso: '2026-09-15',
|
||||
label: '2026年09月15日',
|
||||
@@ -16,9 +18,11 @@ test('buildNewsMorningSearchQueries covers lead, domestic, world, finance, tech,
|
||||
assert.deepEqual(ids, [
|
||||
'lead', 'domestic', 'world', 'finance', 'tech', 'sports', 'hot', 'weather',
|
||||
'google', 'x', 'universities', 'global_markets',
|
||||
'asia_pacific', 'commodities', 'ai_chips', 'policy', 'community', 'science',
|
||||
'culture', 'startup', 'crypto', 'geopolitics', 'local_life', 'official_feeds',
|
||||
]);
|
||||
assert.ok(queries.some((item) => item.id === 'google' && /today latest 2026/u.test(item.query)));
|
||||
assert.ok(queries.some((item) => item.id === 'global_markets' && /global stock market/u.test(item.query)));
|
||||
assert.ok(queries.some((item) => item.id === 'asia_pacific' && /Asia Pacific/u.test(item.query)));
|
||||
assert.ok(queries.some((item) => item.id === 'official_feeds' && /reuters\.com/u.test(item.query)));
|
||||
});
|
||||
|
||||
test('filterFreshNewsMorningSearchResults drops wikinews archive hits', () => {
|
||||
@@ -30,38 +34,71 @@ test('filterFreshNewsMorningSearchResults drops wikinews archive hits', () => {
|
||||
assert.match(filtered[0].title, /国乒/);
|
||||
});
|
||||
|
||||
test('dedupeNewsMorningSearchGroups removes duplicate urls and similar titles across groups', () => {
|
||||
const { groups, removedCount } = dedupeNewsMorningSearchGroups([
|
||||
{
|
||||
id: 'world',
|
||||
title: '国际',
|
||||
results: [{ title: 'Fed raises interest rates', url: 'https://www.reuters.com/world/fed-hike' }],
|
||||
},
|
||||
{
|
||||
id: 'global_markets',
|
||||
title: '全球股市',
|
||||
results: [
|
||||
{ title: 'Fed raises interest rates for first time', url: 'https://www.reuters.com/world/fed-hike/' },
|
||||
{ title: 'Oil prices climb after OPEC move', url: 'https://energy.example/oil' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.equal(removedCount, 1);
|
||||
assert.equal(groups[0].results.length, 1);
|
||||
assert.equal(groups[1].results.length, 1);
|
||||
assert.match(groups[1].results[0].title, /Oil prices/);
|
||||
assert.equal(
|
||||
isDuplicateNewsMorningStory(
|
||||
[{ urlKey: 'reuters.com/world/fed-hike', title: 'fed raises interest rates' }],
|
||||
{ title: 'Fed raises interest rates again', url: 'https://www.reuters.com/world/fed-hike' },
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('formatNewsMorningSearchBrief requires dual search even when empty', () => {
|
||||
const text = formatNewsMorningSearchBrief([
|
||||
{ id: 'domestic', title: '国内热点', results: [] },
|
||||
], { dateLabel: { iso: '2026-09-15' } });
|
||||
], { dateLabel: { iso: '2026-09-15' }, dedupeRemovedCount: 2 });
|
||||
assert.match(text, /专用联网搜索预取/);
|
||||
assert.match(text, /tkmind_search/);
|
||||
assert.match(text, /web_search/);
|
||||
assert.match(text, /禁止只使用 web_search/);
|
||||
assert.match(text, /国内、国际、财经、科技、体育、热搜/);
|
||||
assert.match(text, /全页去重/);
|
||||
assert.match(text, /已剔除 2 条重复/);
|
||||
});
|
||||
|
||||
test('prefetchNewsMorningSearxngBrief queries news category then falls back', async () => {
|
||||
const calls = [];
|
||||
let seq = 0;
|
||||
const brief = await prefetchNewsMorningSearxngBrief({
|
||||
now: Date.parse('2026-09-15T05:00:00+08:00'),
|
||||
timezone: 'Asia/Shanghai',
|
||||
env: { TKMIND_SEARCH_SEARXNG_URL: 'http://127.0.0.1:20080/search' },
|
||||
searchImpl: async (query, options) => {
|
||||
calls.push({ query, categories: options.categories || '' });
|
||||
const slug = `${++seq}-${options.categories || 'general'}`;
|
||||
if (options.categories === 'news' && /国内/.test(query)) {
|
||||
return [];
|
||||
}
|
||||
if (options.categories === 'news') {
|
||||
return [{ title: `${query} 新闻源`, url: 'https://news.example/a', snippet: '摘要' }];
|
||||
return [{ title: `[news-${slug}] 新闻源`, url: `https://news.example/${slug}`, snippet: '摘要' }];
|
||||
}
|
||||
return [{ title: `${query} 通用源`, url: 'https://web.example/b', snippet: '补搜' }];
|
||||
return [{ title: `[web-${slug}] 通用源`, url: `https://web.example/${slug}`, snippet: '补搜' }];
|
||||
},
|
||||
});
|
||||
assert.ok(calls.some((item) => item.categories === 'news'));
|
||||
assert.ok(calls.some((item) => item.categories === '' && /国内/.test(item.query)));
|
||||
assert.match(brief, /国际.*新闻源/);
|
||||
assert.match(brief, /国内.*通用源/);
|
||||
assert.match(brief, /## 国际[\s\S]*新闻源/);
|
||||
assert.match(brief, /## 国内[\s\S]*通用源/);
|
||||
assert.match(brief, /全页去重/);
|
||||
assert.match(brief, /tkmind_search/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user