feat(news-morning): add Google, X, university and global market sources
Expand SearXNG prefetch and template with four English-friendly sections so daily news can surface global headlines without copying stale archives. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Enable global-source news morning template and trigger prod worker regenerate + push.
|
||||
*/
|
||||
import process from 'node:process';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const CONFIG_KEY = 'news_morning_draft';
|
||||
const CONFIG_TABLE = 'h5_wechat_admin_config';
|
||||
|
||||
async function main() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL missing');
|
||||
process.exit(1);
|
||||
}
|
||||
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
||||
const [rows] = await pool.query(
|
||||
`SELECT config_json FROM ${CONFIG_TABLE} WHERE config_key = ? LIMIT 1`,
|
||||
[CONFIG_KEY],
|
||||
);
|
||||
const current = rows[0]?.config_json
|
||||
? (typeof rows[0].config_json === 'string' ? JSON.parse(rows[0].config_json) : rows[0].config_json)
|
||||
: {};
|
||||
const next = {
|
||||
...current,
|
||||
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;禁止整段纯英文正文。',
|
||||
].join('\n'),
|
||||
};
|
||||
const now = Date.now();
|
||||
await pool.query(
|
||||
`INSERT INTO ${CONFIG_TABLE} (config_key, config_json, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_json = VALUES(config_json),
|
||||
updated_by = VALUES(updated_by),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[CONFIG_KEY, JSON.stringify(next), 'global-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.',
|
||||
}, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildNewsMorningAutoGenerationTask,
|
||||
findLatestNewsMorningPage,
|
||||
isLocalScheduleDue,
|
||||
isNewsMorningPageDegraded,
|
||||
isNewsMorningPageForToday,
|
||||
isWithinNewsMorningGenerateLeadWindow,
|
||||
} from './wechat-news-morning-draft.mjs';
|
||||
@@ -157,6 +158,54 @@ export function startWechatNewsMorningDraftWorker({
|
||||
const toleranceMinutes = Math.max(1, Math.ceil(Number(intervalMs) / 60_000));
|
||||
let todayPage = resolveTodayPage(config, h5Root, now);
|
||||
|
||||
const todayPageDegraded = todayPage && isNewsMorningPageDegraded(todayPage);
|
||||
if (
|
||||
todayPageDegraded
|
||||
&& generationInFlightDateKey !== dateKey
|
||||
&& (
|
||||
config.forceGenerateOnce
|
||||
|| isWithinNewsMorningGenerateLeadWindow(config, now)
|
||||
|| isLocalScheduleDue(
|
||||
{ hour: config.pushHour, minute: config.pushMinute, timezone },
|
||||
now,
|
||||
toleranceMinutes + 180,
|
||||
)
|
||||
)
|
||||
) {
|
||||
logger.warn?.('[NewsMorningDraft] today page degraded, regenerating', {
|
||||
slug: todayPage.slug,
|
||||
userId: config.sourceUserId,
|
||||
dateKey,
|
||||
});
|
||||
try {
|
||||
todayPage = await ensureTodayPage(config, dateKey, { now, force: true });
|
||||
if (
|
||||
todayPage
|
||||
&& config.autoPushEnabled
|
||||
&& !isNewsMorningPageDegraded(todayPage)
|
||||
&& isLocalScheduleDue(
|
||||
{ hour: config.pushHour, minute: config.pushMinute, timezone },
|
||||
now,
|
||||
toleranceMinutes + 180,
|
||||
)
|
||||
) {
|
||||
const result = await wechatNewsMorningDraftService.pushDraft({
|
||||
triggeredBy: 'degraded-regenerate',
|
||||
now,
|
||||
});
|
||||
logger.log?.('[NewsMorningDraft] degraded regenerate push succeeded', {
|
||||
draftMediaId: result.draftMediaId,
|
||||
pageSlug: result.preview?.page?.slug ?? todayPage.slug,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn?.(
|
||||
'[NewsMorningDraft] degraded page regenerate failed:',
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.forceGenerateOnce && generationInFlightDateKey !== dateKey) {
|
||||
try {
|
||||
todayPage = await ensureTodayPage(config, dateKey, { now, force: true });
|
||||
|
||||
@@ -30,30 +30,50 @@ const DEFAULT_DRAFT_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/draft/add';
|
||||
const DEFAULT_MATERIAL_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/material/add_material';
|
||||
const DEFAULT_UPLOAD_IMG_URL = 'https://api.weixin.qq.com/cgi-bin/media/uploadimg';
|
||||
const MAX_THUMB_BYTES = 64 * 1024;
|
||||
export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-16';
|
||||
export const NEWS_MORNING_TEMPLATE_REFERENCE_URL =
|
||||
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/daily-news-0915.html';
|
||||
|
||||
/** 0910 新闻早报页面结构说明,供定时任务 taskSpec 与草稿转换共用。 */
|
||||
export const NEWS_MORNING_TEMPLATE_0910_SPEC = [
|
||||
'生成「每日新闻早报」静态 HTML 页面,版式对齐 2026-09-10 生产版(daily-news-MMDD):',
|
||||
'1. Hero 渐变区:主标题「📰 每日新闻早报」、date-badge、subtitle 导语;',
|
||||
'2. quick-links 锚点导航(要闻/天气/国际/国内/财经/科技/体育等);',
|
||||
'3. 多个 section:section-title + card(tag、h3 标题、p 正文、source 来源);',
|
||||
'4. 可选 weather、knowledge、history 等区块;',
|
||||
'5. footer 品牌;',
|
||||
'6. 文件名 daily-news-MMDD.html(如 daily-news-0910.html),并生成同名 .thumbnail.png;',
|
||||
'7. 推送微信草稿时转为内联 style 版式(与 daily-news-0904 公众号已发版一致,微信不支持 <style> 标签),禁止整页长图;底部追加统一「阅读原文 + 二维码」layout。',
|
||||
export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-18-global';
|
||||
|
||||
/** 生产 daily-news-0915 版式:div.section + 扩展全球栏目。 */
|
||||
export const NEWS_MORNING_TEMPLATE_0915_SPEC = [
|
||||
'生成「每日新闻早报」静态 HTML 页面(daily-news-MMDD),版式必须与生产参照页完全一致(仅 HTML/CSS 结构,禁止复制其正文):',
|
||||
`参照页:${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)。',
|
||||
'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=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. 英文栏目允许英文标题或中英并列,但每条必须有中文可读摘要,禁止整段纯英文正文。',
|
||||
].join('\n');
|
||||
|
||||
/** 兼容旧引用名 */
|
||||
export const NEWS_MORNING_TEMPLATE_0910_SPEC = NEWS_MORNING_TEMPLATE_0915_SPEC;
|
||||
|
||||
/** 每日正文必须新搜,禁止把历史早报当新闻来源。 */
|
||||
export const NEWS_MORNING_FRESH_CONTENT_SPEC = [
|
||||
'内容硬约束(必须遵守,优先于版式参考):',
|
||||
'1. 新闻广度必须同时走两条检索,禁止只靠 web_search:先 load_skill → web,再同一轮调用 tkmind_search(type=news,专用联网搜索)和 web_search,合并去重后写页面。',
|
||||
'2. 覆盖面至少包括:国内热点、国际热点、热门事件/热搜;并补财经、科技、体育与当日天气。不得凭记忆编造。',
|
||||
'3. 若任务中附有「专用联网搜索预取」结果,必须优先从中选题,再用 tkmind_search / web_search 补漏;禁止只转述 web_search。',
|
||||
'4. 禁止复制、改写、微调昨日或任何历史 daily-news-*.html 的新闻标题与正文。',
|
||||
'5. 最近一版 daily-news 只允许当作版式参考(hero / section / card 结构);读完后必须丢弃其中的条目内容。',
|
||||
'6. 每条卡片必须是当日可核验事件,并写明来源;title、date-badge、导语必须写明当日日期。要闻栏至少 6 条,其中国内、国际、热门事件都要有代表条目,并用 highlight-box 突出头条。',
|
||||
'7. 必须包含天气区块(section id=weather,weather-today + weather-card)。天气卡片必须写热门旅游城市当日天气(北京、上海、广州、深圳、成都、杭州、西安、重庆、武汉、南京、厦门、三亚等),禁止用区域预报(西部/大部/平原/秋收区)顶替城市卡。若搜索失败,明确写出未能获取当日新闻,禁止用旧稿充数。',
|
||||
'内容硬约束(必须遵守,优先于版式):',
|
||||
'1. 检索必须双路:先 load_skill → web,再同一轮调用 tkmind_search(type=news) 与 web_search,合并去重后写页;禁止只靠 web_search。',
|
||||
'2. 覆盖面至少包括:国内、国际、热搜、财经、科技、体育、天气,以及 Google / X / 全球高校 / 全球股市四个英文栏目;不得凭记忆编造。',
|
||||
'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 结构参考。',
|
||||
].join('\n');
|
||||
|
||||
function normalizeBoolean(value, fallback = false) {
|
||||
@@ -108,6 +128,30 @@ function formatLocalDateParts(date = new Date(), timezone = 'Asia/Shanghai') {
|
||||
};
|
||||
}
|
||||
|
||||
/** 检测「检索失败 / 空内容」降级页,需强制重生成。 */
|
||||
export function isNewsMorningContentDegraded(html, { minStoryCards = 6 } = {}) {
|
||||
const source = String(html ?? '');
|
||||
if (!source.trim()) return true;
|
||||
if (/关于今日早报的重要说明|未填充任何当日具体新闻条目|未能获取当日可核验新闻/u.test(source)) {
|
||||
return true;
|
||||
}
|
||||
const cards = (source.match(/class="card(?:\s|")/gi) ?? []).length;
|
||||
const highlights = (source.match(/class="card highlight-box/gi) ?? []).length;
|
||||
return cards + highlights < minStoryCards;
|
||||
}
|
||||
|
||||
export function isNewsMorningPageDegraded(page, { minStoryCards = 6 } = {}) {
|
||||
if (!page?.localPath || !fs.existsSync(page.localPath)) return false;
|
||||
try {
|
||||
return isNewsMorningContentDegraded(
|
||||
fs.readFileSync(page.localPath, 'utf8'),
|
||||
{ minStoryCards },
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewsMorningPageForToday(
|
||||
page,
|
||||
{ date = new Date(), timezone = 'Asia/Shanghai' } = {},
|
||||
@@ -161,8 +205,10 @@ export function buildNewsMorningAutoGenerationTask(config, { now = Date.now(), s
|
||||
taskSpec: [
|
||||
NEWS_MORNING_FRESH_CONTENT_SPEC,
|
||||
NEWS_MORNING_TEMPLATE_0910_SPEC,
|
||||
`版式参照(只读结构):${NEWS_MORNING_TEMPLATE_REFERENCE_URL}`,
|
||||
`今日日期:${dateLabel.iso}(${dateLabel.year}年${dateLabel.month}月${dateLabel.day}日)。正文必须是这一天的新闻,不能是其它日期的旧稿。`,
|
||||
`输出文件名必须是 daily-news-${dateLabel.mmdd}.html(可覆盖同名旧文件)。`,
|
||||
String(config?.taskSpecAppend ?? '').trim(),
|
||||
brief,
|
||||
].filter(Boolean).join('\n'),
|
||||
recurrence: 'once',
|
||||
@@ -856,6 +902,7 @@ export function createWechatNewsMorningDraftService(
|
||||
pageSlugPattern: String(stored.pageSlugPattern ?? defaults.pageSlugPattern).trim() || defaults.pageSlugPattern,
|
||||
author: String(stored.author ?? defaults.author).trim().slice(0, 8) || defaults.author,
|
||||
forceGenerateOnce: normalizeBoolean(stored.forceGenerateOnce, false),
|
||||
taskSpecAppend: String(stored.taskSpecAppend ?? '').trim(),
|
||||
templateVersion: NEWS_MORNING_TEMPLATE_VERSION,
|
||||
};
|
||||
}
|
||||
@@ -1046,6 +1093,10 @@ export function createWechatNewsMorningDraftService(
|
||||
payload.forceGenerateOnce === undefined
|
||||
? current.forceGenerateOnce
|
||||
: normalizeBoolean(payload.forceGenerateOnce, current.forceGenerateOnce),
|
||||
taskSpecAppend:
|
||||
payload.taskSpecAppend === undefined
|
||||
? current.taskSpecAppend
|
||||
: String(payload.taskSpecAppend ?? '').trim(),
|
||||
templateVersion: NEWS_MORNING_TEMPLATE_VERSION,
|
||||
};
|
||||
const now = Date.now();
|
||||
@@ -1299,6 +1350,8 @@ export const wechatNewsMorningDraftInternals = {
|
||||
buildDailyNewsWechatDraftArticle,
|
||||
findLatestNewsMorningPage,
|
||||
isNewsMorningPageForToday,
|
||||
isNewsMorningContentDegraded,
|
||||
isNewsMorningPageDegraded,
|
||||
isLocalScheduleDue,
|
||||
isWithinNewsMorningGenerateLeadWindow,
|
||||
buildNewsMorningAutoGenerationTask,
|
||||
|
||||
@@ -14,6 +14,7 @@ function formatLocalDateParts(date = new Date(), timezone = 'Asia/Shanghai') {
|
||||
const month = String(parts.month).padStart(2, '0');
|
||||
const day = String(parts.day).padStart(2, '0');
|
||||
return {
|
||||
year,
|
||||
iso: `${year}-${month}-${day}`,
|
||||
label: `${year}年${month}月${day}日`,
|
||||
};
|
||||
@@ -26,15 +27,44 @@ export function shouldPrefetchNewsMorningSearxng(env = process.env) {
|
||||
export function buildNewsMorningSearchQueries(dateLabel) {
|
||||
const day = String(dateLabel?.iso ?? '').trim();
|
||||
const label = String(dateLabel?.label ?? day).trim() || day;
|
||||
const year = String(dateLabel?.year ?? day.slice(0, 4) ?? new Date().getFullYear()).trim();
|
||||
// 用「今天/最新 + 年」避免命中 Wikinews 历史「X月X日头版」归档页。
|
||||
const fresh = `今天 最新 ${year}年`;
|
||||
const englishFresh = `today latest ${year}`;
|
||||
return [
|
||||
{ id: 'domestic', title: '国内热点', query: `${label} 国内热点新闻` },
|
||||
{ id: 'world', title: '国际热点', query: `${label} 国际热点新闻` },
|
||||
{ id: 'hot', title: '热门事件', query: `${label} 热门事件 热点` },
|
||||
{ id: 'finance', title: '财经科技', query: `${label} 财经 科技 新闻` },
|
||||
{ id: 'weather', title: '天气', query: `${label} 全国天气预报` },
|
||||
{ id: 'lead', title: '头条必读', query: `${fresh} 突发 重要 新闻` },
|
||||
{ id: 'domestic', title: '国内', query: `${fresh} 中国 国内 热点 新闻` },
|
||||
{ id: 'world', title: '国际', query: `${fresh} 国际 全球 热点 新闻` },
|
||||
{ id: 'finance', title: '财经', query: `${fresh} 财经 金融 A股 股市` },
|
||||
{ id: 'tech', title: '科技', query: `${fresh} 科技 AI 创新 发布` },
|
||||
{ id: 'sports', title: '体育', query: `${fresh} 体育 赛事 比赛 结果` },
|
||||
{ id: 'hot', title: '热搜', query: `${fresh} 热搜 热门 话题 事件` },
|
||||
{ id: 'weather', title: '天气', query: `${label} 全国 主要城市 天气预报` },
|
||||
{ id: 'google', title: 'Google 热点', query: `${englishFresh} top news headlines site:reuters.com OR site:bbc.com OR site:apnews.com` },
|
||||
{ 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` },
|
||||
].filter((item) => item.query.trim());
|
||||
}
|
||||
|
||||
const STALE_NEWS_PATTERNS = [
|
||||
/wikinews\.org\/wiki\/\d{4}年/u,
|
||||
/中文報[紙纸]頭條/u,
|
||||
/大事回顧|大事回顾/u,
|
||||
/(\d{4})年(\d{1,2})月(\d{1,2})日.*(\d{4})年(\d{1,2})月(\d{1,2})日/u,
|
||||
];
|
||||
|
||||
export function filterFreshNewsMorningSearchResults(results = [], { year = null } = {}) {
|
||||
const currentYear = Number(year ?? new Date().getFullYear());
|
||||
return (Array.isArray(results) ? results : []).filter((item) => {
|
||||
const blob = `${item?.title ?? ''} ${item?.url ?? ''} ${item?.snippet ?? ''}`;
|
||||
if (STALE_NEWS_PATTERNS.some((pattern) => pattern.test(blob))) return false;
|
||||
const years = [...blob.matchAll(/\b(20\d{2})\b/g)].map((match) => Number(match[1]));
|
||||
if (years.length && years.every((value) => value < currentYear - 1)) return false;
|
||||
return Boolean(item?.title || item?.url);
|
||||
});
|
||||
}
|
||||
|
||||
function truncateSnippet(value, max = 90) {
|
||||
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return '';
|
||||
@@ -44,7 +74,7 @@ function truncateSnippet(value, max = 90) {
|
||||
export function formatNewsMorningSearchBrief(groups = [], { dateLabel = null } = {}) {
|
||||
const lines = [
|
||||
'【专用联网搜索预取】以下结果来自专用新闻检索(与 web_search 并列,不是替代)。',
|
||||
'写页面时必须:1)优先从下列结果覆盖国内、国际、热门事件;2)同一轮再调用 tkmind_search(type=news) 与 web_search 补漏并核对来源;3)禁止只使用 web_search。',
|
||||
'写页面时必须:1)优先从下列结果覆盖头条/国内/国际/财经/科技/体育/热搜,以及 Google 热点、X 平台、全球高校、全球股市四个英文栏目;2)同一轮再调用 tkmind_search(type=news) 与 web_search 补漏并核对来源;3)禁止只使用 web_search;4)英文栏目可保留英文标题,正文用中文摘要并附原文链接。',
|
||||
];
|
||||
if (dateLabel?.iso) lines.push(`预取日期:${dateLabel.iso}`);
|
||||
lines.push('');
|
||||
@@ -70,7 +100,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();
|
||||
}
|
||||
@@ -119,13 +149,17 @@ export async function prefetchNewsMorningSearxngBrief({
|
||||
}
|
||||
const timeoutMs = Number(env.TKMIND_SEARCH_TIMEOUT_MS ?? 8000) || 8000;
|
||||
const groups = await Promise.all(queries.map(async (item) => {
|
||||
const results = await searchNewsThenGeneral(item.query, {
|
||||
const raw = await searchNewsThenGeneral(item.query, {
|
||||
endpoint,
|
||||
searchImpl,
|
||||
limit,
|
||||
limit: limit + 4,
|
||||
timeoutMs,
|
||||
});
|
||||
return { ...item, results: Array.isArray(results) ? results : [] };
|
||||
const results = filterFreshNewsMorningSearchResults(
|
||||
Array.isArray(raw) ? raw : [],
|
||||
{ year: dateLabel.year },
|
||||
).slice(0, limit);
|
||||
return { ...item, results };
|
||||
}));
|
||||
return formatNewsMorningSearchBrief(groups, { dateLabel });
|
||||
}
|
||||
|
||||
@@ -2,18 +2,32 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildNewsMorningSearchQueries,
|
||||
filterFreshNewsMorningSearchResults,
|
||||
formatNewsMorningSearchBrief,
|
||||
prefetchNewsMorningSearxngBrief,
|
||||
} from './wechat-news-morning-search.mjs';
|
||||
|
||||
test('buildNewsMorningSearchQueries covers domestic, world and hot events', () => {
|
||||
test('buildNewsMorningSearchQueries covers lead, domestic, world, finance, tech, sports and hot', () => {
|
||||
const queries = buildNewsMorningSearchQueries({
|
||||
iso: '2026-09-15',
|
||||
label: '2026年09月15日',
|
||||
});
|
||||
const ids = queries.map((item) => item.id);
|
||||
assert.deepEqual(ids, ['domestic', 'world', 'hot', 'finance', 'weather']);
|
||||
assert.ok(queries.every((item) => item.query.includes('2026年09月15日')));
|
||||
assert.deepEqual(ids, [
|
||||
'lead', 'domestic', 'world', 'finance', 'tech', 'sports', 'hot', 'weather',
|
||||
'google', 'x', 'universities', 'global_markets',
|
||||
]);
|
||||
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)));
|
||||
});
|
||||
|
||||
test('filterFreshNewsMorningSearchResults drops wikinews archive hits', () => {
|
||||
const filtered = filterFreshNewsMorningSearchResults([
|
||||
{ title: '2021年9月18日中文報紙頭條', url: 'https://zh.wikinews.org/wiki/2021%E5%B9%B49%E6%9C%8818%E6%97%A5' },
|
||||
{ title: '国乒今日出征亚运会', url: 'https://news.qq.com/a/20260918' },
|
||||
], { year: 2026 });
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.match(filtered[0].title, /国乒/);
|
||||
});
|
||||
|
||||
test('formatNewsMorningSearchBrief requires dual search even when empty', () => {
|
||||
@@ -24,7 +38,7 @@ test('formatNewsMorningSearchBrief requires dual search even when empty', () =>
|
||||
assert.match(text, /tkmind_search/);
|
||||
assert.match(text, /web_search/);
|
||||
assert.match(text, /禁止只使用 web_search/);
|
||||
assert.match(text, /国内、国际、热门事件/);
|
||||
assert.match(text, /国内、国际、财经、科技、体育、热搜/);
|
||||
});
|
||||
|
||||
test('prefetchNewsMorningSearxngBrief queries news category then falls back', async () => {
|
||||
@@ -35,7 +49,7 @@ test('prefetchNewsMorningSearxngBrief queries news category then falls back', as
|
||||
env: { TKMIND_SEARCH_SEARXNG_URL: 'http://127.0.0.1:20080/search' },
|
||||
searchImpl: async (query, options) => {
|
||||
calls.push({ query, categories: options.categories || '' });
|
||||
if (options.categories === 'news' && /国内热点/.test(query)) {
|
||||
if (options.categories === 'news' && /国内/.test(query)) {
|
||||
return [];
|
||||
}
|
||||
if (options.categories === 'news') {
|
||||
@@ -45,9 +59,9 @@ test('prefetchNewsMorningSearxngBrief queries news category then falls back', as
|
||||
},
|
||||
});
|
||||
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.ok(calls.some((item) => item.categories === '' && /国内/.test(item.query)));
|
||||
assert.match(brief, /国际.*新闻源/);
|
||||
assert.match(brief, /国内.*通用源/);
|
||||
assert.match(brief, /tkmind_search/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user