Compare commits

..

2 Commits

Author SHA1 Message Date
john 799afd1ea5 feat(admin): auto-load today news morning preview in WeChat page
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 08:52:58 +08:00
john 04d68d0b2c docs(admin): clarify news morning draft auto push runs on Portal
Update the WeChat admin copy to reflect Portal-side scheduled generation and draft push.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 08:48:01 +08:00
4 changed files with 162 additions and 4 deletions
+20
View File
@@ -455,6 +455,26 @@ export function createAdminApp(services) {
}
});
adminApi.get('/wechat/news-morning-draft/today', requireAdmin, async (_req, res) => {
if (!wechatNewsMorningDraftService) {
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
}
return res.json(await wechatNewsMorningDraftService.getTodayStatus());
});
adminApi.get('/wechat/news-morning-draft/preview/today', requireAdmin, async (_req, res) => {
if (!wechatNewsMorningDraftService) {
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
}
try {
return res.json(await wechatNewsMorningDraftService.previewToday());
} catch (error) {
return res.status(400).json({
message: error instanceof Error ? error.message : '预览失败',
});
}
});
adminApi.post('/wechat/news-morning-draft/push', requireAdmin, async (req, res) => {
if (!wechatNewsMorningDraftService) {
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
+111 -4
View File
@@ -8,6 +8,7 @@ import {
getWechatIntentRouterConfig,
getWechatIntentRouterRuntime,
getWechatNewsMorningDraftConfig,
getWechatNewsMorningDraftTodayStatus,
listAdminUsers,
listLlmProviderKeys,
listWechatBindings,
@@ -18,7 +19,7 @@ import {
listWechatWebNotifications,
patchWechatIntentRouterConfig,
patchWechatNewsMorningDraftConfig,
previewWechatNewsMorningDraft,
previewWechatNewsMorningDraftToday,
pushWechatNewsMorningDraft,
resumeWechatDigest,
updateWechatScheduleLlmConfig,
@@ -36,6 +37,7 @@ import type {
WechatNewsMorningDraftPreview,
WechatNewsMorningDraftRun,
WechatNewsMorningDraftTemplate,
WechatNewsMorningDraftTodayStatus,
WechatWebNotification,
} from '../../types';
import { formatTime } from '../utils/format';
@@ -188,6 +190,7 @@ export function WechatPage() {
const [savedNewsDraftForm, setSavedNewsDraftForm] = useState<NewsMorningDraftForm | null>(null);
const [newsDraftTemplate, setNewsDraftTemplate] = useState<WechatNewsMorningDraftTemplate | null>(null);
const [newsDraftPreview, setNewsDraftPreview] = useState<WechatNewsMorningDraftPreview | null>(null);
const [newsDraftTodayStatus, setNewsDraftTodayStatus] = useState<WechatNewsMorningDraftTodayStatus | null>(null);
const [newsDraftRuns, setNewsDraftRuns] = useState<WechatNewsMorningDraftRun[]>([]);
const [newsDraftSaving, setNewsDraftSaving] = useState(false);
const [newsDraftWorking, setNewsDraftWorking] = useState(false);
@@ -274,10 +277,40 @@ export function WechatPage() {
}
}, [deliveryStatus, digestStatus, messageStatus, notificationStatus, search]);
const refreshTodayNewsMorningDraft = useCallback(async (sourceUserId?: string) => {
if (!sourceUserId?.trim()) {
setNewsDraftTodayStatus(null);
return;
}
try {
const status = await getWechatNewsMorningDraftTodayStatus();
setNewsDraftTodayStatus(status);
if (status.todayPageReady) {
setNewsDraftPreview(await previewWechatNewsMorningDraftToday());
} else {
setNewsDraftPreview(null);
}
} catch (err) {
setNewsDraftTodayStatus(null);
if (!(err instanceof Error && /404|503/u.test(err.message))) {
setError(err instanceof Error ? err.message : '加载今日早报失败');
}
}
}, []);
useEffect(() => {
void load();
}, [load]);
useEffect(() => {
if (!newsDraftForm?.sourceUserId.trim()) {
setNewsDraftTodayStatus(null);
setNewsDraftPreview(null);
return;
}
void refreshTodayNewsMorningDraft(newsDraftForm.sourceUserId);
}, [newsDraftForm?.sourceUserId, refreshTodayNewsMorningDraft]);
const runAction = async (action: () => Promise<unknown>) => {
setBusy(true);
setError(null);
@@ -405,7 +438,14 @@ export function WechatPage() {
setNewsDraftWorking(true);
setError(null);
try {
setNewsDraftPreview(await previewWechatNewsMorningDraft());
const status = await getWechatNewsMorningDraftTodayStatus();
setNewsDraftTodayStatus(status);
if (status.todayPageReady) {
setNewsDraftPreview(await previewWechatNewsMorningDraftToday());
} else {
setNewsDraftPreview(null);
setError(status.message ?? '今日新闻早报尚未生成');
}
} catch (err) {
setError(err instanceof Error ? err.message : '预览失败');
} finally {
@@ -442,6 +482,7 @@ export function WechatPage() {
setNotice(`草稿已写入微信后台,media_id${result.draftMediaId ?? '—'}`);
setNewsDraftPreview(result.preview ?? null);
setNewsDraftRuns(await listWechatNewsMorningDraftRuns(10));
await refreshTodayNewsMorningDraft(newsDraftForm.sourceUserId);
} catch (err) {
setError(err instanceof Error ? err.message : '推送失败');
setNotice(null);
@@ -579,6 +620,72 @@ export function WechatPage() {
{newsDraftSaving ? '保存中…' : newsDraftDirty ? '保存配置' : '已保存'}
</button>
</div>
{newsDraftTodayStatus ? (
<div
className="admin-card"
style={{ marginTop: 16, background: 'var(--panel-2, rgba(255,255,255,.03))' }}
>
<div className="admin-card-head">
<div>
<h3 style={{ marginBottom: 4 }}> · {newsDraftTodayStatus.dateKey}</h3>
<p className="muted" style={{ marginTop: 0 }}>
<code>{newsDraftTodayStatus.expectedSlug}.html</code>
</p>
</div>
<button
type="button"
className="ghost-btn"
onClick={() => void refreshTodayNewsMorningDraft(newsDraftForm.sourceUserId)}
disabled={busy || newsDraftWorking || !newsDraftForm.sourceUserId.trim()}
>
</button>
</div>
<dl className="admin-dl">
<div>
<dt></dt>
<dd className={newsDraftTodayStatus.todayPageReady ? 'wechat-ok' : 'text-error'}>
{newsDraftTodayStatus.todayPageReady ? '今日页面已生成' : newsDraftTodayStatus.message ?? '待生成'}
</dd>
</div>
{newsDraftTodayStatus.page ? (
<>
<div>
<dt></dt>
<dd>
<a href={newsDraftTodayStatus.page.publicUrl} target="_blank" rel="noreferrer">
{newsDraftTodayStatus.page.slug}
</a>
</dd>
</div>
<div>
<dt></dt>
<dd>{dateLabel(newsDraftTodayStatus.page.modifiedAt)}</dd>
</div>
</>
) : newsDraftTodayStatus.latestPageSlug ? (
<div>
<dt></dt>
<dd className="muted">{newsDraftTodayStatus.latestPageSlug}</dd>
</div>
) : null}
{newsDraftTodayStatus.autoPushEnabled ? (
<div>
<dt></dt>
<dd>
{String(newsDraftTodayStatus.pushHour ?? 6).padStart(2, '0')}:
{String(newsDraftTodayStatus.pushMinute ?? 0).padStart(2, '0')}
{newsDraftTodayStatus.runs.push?.status === 'success'
? ' · 今日已推送草稿'
: newsDraftTodayStatus.runs.push?.status === 'failed'
? ` · 推送失败:${newsDraftTodayStatus.runs.push.errorMessage ?? '—'}`
: ' · 待推送'}
</dd>
</div>
) : null}
</dl>
</div>
) : null}
<div className="admin-form" style={{ marginTop: 16 }}>
<label>
<input
@@ -600,7 +707,7 @@ export function WechatPage() {
setNewsDraftForm((current) => current && { ...current, autoPushEnabled: event.target.checked })
}
/>{' '}
Portal
Portal 1 稿
</label>
<label>
@@ -709,7 +816,7 @@ export function WechatPage() {
) : null}
{newsDraftPreview ? (
<div className="admin-card" style={{ marginTop: 16, background: 'var(--panel-2, rgba(255,255,255,.03))' }}>
<h3 style={{ marginBottom: 8 }}></h3>
<h3 style={{ marginBottom: 8 }}>稿</h3>
<dl className="admin-dl">
<div>
<dt></dt>
+9
View File
@@ -59,6 +59,7 @@ import type {
WechatNewsMorningDraftPreview,
WechatNewsMorningDraftRun,
WechatNewsMorningDraftTemplate,
WechatNewsMorningDraftTodayStatus,
WechatIntentRouterAdminConfig,
WechatIntentRouterConfigState,
WechatIntentRouterRuntimeState,
@@ -486,6 +487,14 @@ export async function previewWechatNewsMorningDraft(): Promise<WechatNewsMorning
return portalFetch<WechatNewsMorningDraftPreview>('/admin-api/wechat/news-morning-draft/preview');
}
export async function getWechatNewsMorningDraftTodayStatus(): Promise<WechatNewsMorningDraftTodayStatus> {
return portalFetch<WechatNewsMorningDraftTodayStatus>('/admin-api/wechat/news-morning-draft/today');
}
export async function previewWechatNewsMorningDraftToday(): Promise<WechatNewsMorningDraftPreview> {
return portalFetch<WechatNewsMorningDraftPreview>('/admin-api/wechat/news-morning-draft/preview/today');
}
export async function pushWechatNewsMorningDraft(options?: { dryRun?: boolean }): Promise<{
ok?: boolean;
dryRun?: boolean;
+22
View File
@@ -357,6 +357,9 @@ export type WechatNewsMorningDraftPreview = {
publicUrl: string;
modifiedAt: number;
hasThumb: boolean;
isTodayPage?: boolean;
dateKey?: string;
expectedSlug?: string;
};
article: {
title: string;
@@ -384,6 +387,25 @@ export type WechatNewsMorningDraftRun = {
createdAt: number;
};
export type WechatNewsMorningDraftTodayStatus = {
configured: boolean;
dateKey: string;
expectedSlug: string;
todayPageReady: boolean;
page: WechatNewsMorningDraftPreview['page'] | null;
latestPageSlug?: string | null;
autoGenerateEnabled?: boolean;
autoPushEnabled?: boolean;
pushHour?: number;
pushMinute?: number;
generateLeadMinutes?: number;
message?: string | null;
runs: {
generate: WechatNewsMorningDraftRun | null;
push: WechatNewsMorningDraftRun | null;
};
};
export type WechatIntentRouterAdminConfig = {
enabled: boolean;
shadowMode: boolean;