diff --git a/.env.example b/.env.example
index 99403ab..9ac35e4 100644
--- a/.env.example
+++ b/.env.example
@@ -230,6 +230,7 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
# H5_SCHEDULED_TASK_MAX_ATTEMPTS=3
# H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS=900000
# 新闻早报 · 微信草稿箱自动 worker(默认跟随 H5_REMINDER_WORKER_ENABLED)
+# 每天自动生成并推草稿:ENABLED + AUTO + AUTO_GENERATE 都要开;时刻以 DB 配置为准。
# H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED=1
# H5_WECHAT_NEWS_MORNING_DRAFT_AUTO=1
# H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED=1
@@ -530,6 +531,11 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
# Template source: /Users/john/Project/searxng-prod/settings.yml.template
# Goosed MCP endpoints are automatically rewritten to host.docker.internal.
# TKMIND_SEARCH_SEARXNG_URL=http://127.0.0.1:20080/search
+# 早报预取:默认走通用搜索(Yandex/Bing/360),最多 3 路并行,避免打满连接池和触发验证码。
+# MEMIND_NEWS_MORNING_SEARXNG_PREFETCH=1
+# MEMIND_NEWS_MORNING_SEARXNG_NEWS_CATEGORY=0
+# MEMIND_NEWS_MORNING_SEARXNG_CONCURRENCY=3
+# MEMIND_NEWS_MORNING_SEARXNG_ENGINES=yandex,bing,360search
# Portal 在 web / search-enhanced 技能执行前预取 SearXNG;设为 0 可关闭。
# MEMIND_LIVE_SEARCH_PREFETCH=1
# TKMIND_SEARCH_MCP_HOST_GATEWAY=host.docker.internal
diff --git a/docs/branch-disposition.md b/docs/branch-disposition.md
index 113b7bc..79888d2 100644
--- a/docs/branch-disposition.md
+++ b/docs/branch-disposition.md
@@ -1,5 +1,27 @@
# 历史分支处置登记
+## `feature/searxng-news-morning-throttle`
+
+**状态:禁止再次引用。已 fast-forward 并入 `origin/main` 并完成 103 Portal 快速发布。**
+
+审计日期:2026-09-22
+分支 HEAD:`b74ab4ec`
+`origin/main` 对应提交:`b74ab4ec`
+
+### 交付内容
+
+- news002 微信草稿与静态页统一左上角小图(72~92px 缩略图)
+- 远程配图本地化到当日 MindSpace `public/`,缺图保留文字卡、禁止默认占位小图
+- SearXNG 预取与配图 enrichment 链路收紧
+
+### 103 发布记录
+
+- Portal runtime:`20260922-074006-b74ab4ec`(快速发布,`STUDIO_HOST=10.10.0.2`)
+- git_head:`b74ab4ec46924be05c6928438816de5163079dc9`
+- 发布后:Portal 200;`/auth/login` 401;105 隧道端口 19081 健康
+
+---
+
## `feature/wechat-push-secret-codes`
**状态:禁止再次引用。已 fast-forward 并入 `origin/main` 并完成 103 Portal 快速发布。**
diff --git a/docs/news-morning-templates.md b/docs/news-morning-templates.md
index 5ee7647..b665bb1 100644
--- a/docs/news-morning-templates.md
+++ b/docs/news-morning-templates.md
@@ -60,9 +60,9 @@ prompt 层的「禁止重复」无法验证,所以在落盘后再做一次程
链路:SearXNG `img_src`/`thumbnail` → `normalizeSearchResult()` 透传 `image`/`publishedAt`
→ 预取 brief 里以 `图:{URL}` 单独成行 → Agent 写进 `.card .media > img`。
-- 有图:`loading="lazy"` + `referrerpolicy="no-referrer"` + `onerror` 降级为占位
-- 无图:`
{栏目 emoji}
`,版式不塌
-- 禁止张冠李戴、禁止编造图片地址、禁止整页长图
+- 有真实图:必须显示,`loading="lazy"` + `referrerpolicy="no-referrer"`
+- 无图:先抓来源页 `og:image` / `twitter:image`;仍没有就不放配图,保留文字卡
+- 禁止默认小图凑数、禁止张冠李戴、禁止编造图片地址、禁止整页长图
### 4. 视觉与排版
@@ -99,11 +99,14 @@ CSS 固化在 `NEWS002_STYLE_BLOCK`,直接内嵌进 taskSpec,Agent 照抄即
| 导语 | `.subtitle` | `.lede` |
news002 的卡片图会先上传到微信正文素材接口,再由 inline 渲染器输出微信 CDN 地址。
-任一新闻卡缺少真实同源图片、图片上传失败、头条不是 3~4 条、事件卡超过 28 条,
-或页面仍混有 news001 DOM,都会由生成/推送闸门阻断,不允许进入草稿箱。
+静态页与微信草稿均使用 **左上角 72~92px 小图**(`.card` / `.lead-card` 左侧缩略图),不再用全宽大图。
+能拿到真实配图的卡片必须显示并上传;没有真实图就只保留文字卡,不放默认小图。
+远程配图会先下载到当日 MindSpace `public/`,改写成本站相对 URL,避免海外图源在国内打不开。
+已有配图上传失败、头条不是 3~4 条、事件卡超过 28 条,或页面仍混有 news001 DOM,
+才会由生成/推送闸门阻断,不允许进入草稿箱。
-真实图片来源按优先级取文章来源的 `og:image:secure_url`、`og:image`、
-`twitter:image`;不使用与事件无法核验关系的泛图片搜索结果。
+真实图片来源按优先级取 SearXNG 结果里的 `img_src`/`thumbnail`,以及文章来源的
+`og:image:secure_url`、`og:image`、`twitter:image`;不使用与事件无法核验关系的泛图片搜索结果。
## 预览与验证
diff --git a/mindsearch-providers.mjs b/mindsearch-providers.mjs
index a79aa7a..44c29c2 100644
--- a/mindsearch-providers.mjs
+++ b/mindsearch-providers.mjs
@@ -9,6 +9,7 @@ export async function searchSearxng(query, {
signal,
categories = '',
language = '',
+ engines = '',
} = {}) {
if (!endpoint) throw new Error('SearXNG endpoint is not configured');
try { if (!['http:', 'https:'].includes(new URL(endpoint).protocol)) throw new Error('unsupported protocol'); } catch { throw new Error('SearXNG endpoint is not configured safely'); }
@@ -17,6 +18,7 @@ export async function searchSearxng(query, {
url.searchParams.set('format', 'json');
if (categories) url.searchParams.set('categories', String(categories));
if (language) url.searchParams.set('language', String(language));
+ if (engines) url.searchParams.set('engines', String(engines));
const timeout = AbortSignal.timeout(timeoutMs);
const response = await fetchImpl(url, {
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
diff --git a/mindsearch.test.mjs b/mindsearch.test.mjs
index 7fb5d4f..d6a8d4e 100644
--- a/mindsearch.test.mjs
+++ b/mindsearch.test.mjs
@@ -215,6 +215,19 @@ test('SearXNG adapter can request news category and language', async () => {
assert.match(requested, /language=zh-CN/);
});
+test('SearXNG adapter can pin engines', async () => {
+ let requested;
+ await searchSearxng('今日热点', {
+ endpoint: 'http://search.local/search',
+ engines: 'yandex,bing',
+ fetchImpl: async (url) => {
+ requested = String(url);
+ return { ok: true, json: async () => ({ results: [] }) };
+ },
+ });
+ assert.match(requested, /engines=yandex%2Cbing|engines=yandex,bing/);
+});
+
test('GitHub code adapter sends bounded queries and normalizes results', async () => {
let requested;
const result = await searchGithubCode('repo:openai goose', { limit: 50, fetchImpl: async (url, options) => { requested = { url: String(url), options }; return { ok: true, json: async () => ({ items: [{ repository: { full_name: 'openai/goose', description: 'agent' }, path: 'src/goose.rs', html_url: 'https://github.com/openai/goose/blob/main/src/goose.rs' }] }) }; } });
diff --git a/news-morning-templates.mjs b/news-morning-templates.mjs
index ef68835..01910bd 100644
--- a/news-morning-templates.mjs
+++ b/news-morning-templates.mjs
@@ -186,15 +186,16 @@ export const NEWS002_STYLE_BLOCK = `