Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c6427e123 | |||
| 8651a50cbe | |||
| b414b732c8 | |||
| d4127bd24b |
@@ -168,6 +168,7 @@ rsync -a \
|
||||
--include '*.mjs' \
|
||||
--include '*.json' \
|
||||
--include '*.js' \
|
||||
--include 'wechat/assets/*.png' \
|
||||
--exclude '*' \
|
||||
"${MEMIND_SRC}/" "${MEMIND_LIB}/"
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ export function createAdminApp(services) {
|
||||
skillRuntimeConfigService,
|
||||
billingConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
wechatNewsMorningDraftService,
|
||||
wechatIntentRouterConfigService,
|
||||
wechatCursorExecutorPolicyService,
|
||||
adminSystemTestService,
|
||||
@@ -420,6 +421,68 @@ export function createAdminApp(services) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/wechat/news-morning-draft/config', requireAdmin, async (_req, res) => {
|
||||
if (!wechatNewsMorningDraftService) {
|
||||
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
|
||||
}
|
||||
const [config, template] = await Promise.all([
|
||||
wechatNewsMorningDraftService.getConfig(),
|
||||
Promise.resolve(wechatNewsMorningDraftService.getTemplate()),
|
||||
]);
|
||||
return res.json({ config, template });
|
||||
});
|
||||
|
||||
adminApi.patch('/wechat/news-morning-draft/config', requireAdmin, async (req, res) => {
|
||||
if (!wechatNewsMorningDraftService) {
|
||||
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
|
||||
}
|
||||
const config = await wechatNewsMorningDraftService.updateConfig(req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
return res.json({ config });
|
||||
});
|
||||
|
||||
adminApi.get('/wechat/news-morning-draft/preview', requireAdmin, async (_req, res) => {
|
||||
if (!wechatNewsMorningDraftService) {
|
||||
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
|
||||
}
|
||||
try {
|
||||
return res.json(await wechatNewsMorningDraftService.preview());
|
||||
} 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: '新闻早报草稿推送未启用' });
|
||||
}
|
||||
try {
|
||||
const result = await wechatNewsMorningDraftService.pushDraft({
|
||||
triggeredBy: req.currentUser.id,
|
||||
dryRun: req.body?.dryRun === true,
|
||||
});
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '推送失败',
|
||||
run: error?.run ?? null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/wechat/news-morning-draft/runs', requireAdmin, async (req, res) => {
|
||||
if (!wechatNewsMorningDraftService) {
|
||||
return res.status(503).json({ message: '新闻早报草稿推送未启用' });
|
||||
}
|
||||
const runs = await wechatNewsMorningDraftService.listRuns({
|
||||
limit: req.query.limit,
|
||||
});
|
||||
return res.json({ runs });
|
||||
});
|
||||
|
||||
adminApi.get('/wechat/intent-router/config', requireAdmin, async (_req, res) => {
|
||||
if (!wechatIntentRouterConfigService?.getConfig) {
|
||||
return res.status(503).json({ message: '微信意图路由配置未启用' });
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function bootstrapAdminServices() {
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||
const { createWechatNewsMorningDraftService } = await importMemind('wechat-news-morning-draft.mjs');
|
||||
const { createWechatIntentRouterConfigService } = await importMemind('wechat-intent-router-config.mjs');
|
||||
const { createWechatCursorExecutorAdminConfigService } = await importMemind(
|
||||
'wechat-cursor-executor-admin-config.mjs',
|
||||
@@ -138,6 +139,12 @@ export async function bootstrapAdminServices() {
|
||||
portalBaseUrl: `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`,
|
||||
});
|
||||
const wechatMpConfig = loadWechatMpConfig();
|
||||
const wechatNewsMorningDraftService = createWechatNewsMorningDraftService(pool, {
|
||||
mpConfig: wechatMpConfig,
|
||||
h5Root,
|
||||
memindLibRoot: resolveMemindLib(),
|
||||
env: process.env,
|
||||
});
|
||||
const wechatMpService = createWechatMpService({
|
||||
config: wechatMpConfig,
|
||||
userAuth,
|
||||
@@ -204,6 +211,7 @@ export async function bootstrapAdminServices() {
|
||||
skillRuntimeConfigService,
|
||||
billingConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
wechatNewsMorningDraftService,
|
||||
wechatIntentRouterConfigService,
|
||||
wechatCursorExecutorPolicyService,
|
||||
adminSystemTestService,
|
||||
|
||||
@@ -112,6 +112,7 @@ ready
|
||||
skillRuntimeConfigService,
|
||||
billingConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
wechatNewsMorningDraftService,
|
||||
wechatIntentRouterConfigService,
|
||||
wechatCursorExecutorPolicyService,
|
||||
adminSystemTestService,
|
||||
@@ -144,6 +145,7 @@ ready
|
||||
skillRuntimeConfigService,
|
||||
billingConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
wechatNewsMorningDraftService,
|
||||
wechatIntentRouterConfigService,
|
||||
wechatCursorExecutorPolicyService,
|
||||
adminSystemTestService,
|
||||
|
||||
@@ -7,14 +7,19 @@ import {
|
||||
getWechatAdminSummary,
|
||||
getWechatIntentRouterConfig,
|
||||
getWechatIntentRouterRuntime,
|
||||
getWechatNewsMorningDraftConfig,
|
||||
listAdminUsers,
|
||||
listLlmProviderKeys,
|
||||
listWechatBindings,
|
||||
listWechatDeliveries,
|
||||
listWechatDigests,
|
||||
listWechatMessages,
|
||||
listWechatNewsMorningDraftRuns,
|
||||
listWechatWebNotifications,
|
||||
patchWechatIntentRouterConfig,
|
||||
patchWechatNewsMorningDraftConfig,
|
||||
previewWechatNewsMorningDraft,
|
||||
pushWechatNewsMorningDraft,
|
||||
resumeWechatDigest,
|
||||
updateWechatScheduleLlmConfig,
|
||||
} from '../../api/client';
|
||||
@@ -28,6 +33,9 @@ import type {
|
||||
WechatIntentRouterAdminConfig,
|
||||
WechatIntentRouterRuntimeState,
|
||||
WechatMessage,
|
||||
WechatNewsMorningDraftPreview,
|
||||
WechatNewsMorningDraftRun,
|
||||
WechatNewsMorningDraftTemplate,
|
||||
WechatWebNotification,
|
||||
} from '../../types';
|
||||
import { formatTime } from '../utils/format';
|
||||
@@ -88,6 +96,35 @@ type IntentRouterForm = {
|
||||
canaryOpenids: string;
|
||||
};
|
||||
|
||||
type NewsMorningDraftForm = {
|
||||
enabled: boolean;
|
||||
autoPushEnabled: boolean;
|
||||
pushHour: string;
|
||||
pushMinute: string;
|
||||
timezone: string;
|
||||
sourceUserId: string;
|
||||
pageSlugPattern: string;
|
||||
author: string;
|
||||
templateVersion: string;
|
||||
};
|
||||
|
||||
function newsMorningDraftToForm(
|
||||
config: Partial<NewsMorningDraftForm> | undefined,
|
||||
template: WechatNewsMorningDraftTemplate | undefined,
|
||||
): NewsMorningDraftForm {
|
||||
return {
|
||||
enabled: Boolean(config?.enabled),
|
||||
autoPushEnabled: Boolean(config?.autoPushEnabled),
|
||||
pushHour: String(config?.pushHour ?? 6),
|
||||
pushMinute: String(config?.pushMinute ?? 0),
|
||||
timezone: String(config?.timezone ?? 'Asia/Shanghai'),
|
||||
sourceUserId: String(config?.sourceUserId ?? ''),
|
||||
pageSlugPattern: String(config?.pageSlugPattern ?? 'daily-news-*'),
|
||||
author: String(config?.author ?? 'TKMind'),
|
||||
templateVersion: String(config?.templateVersion ?? template?.version ?? '2026-09-10'),
|
||||
};
|
||||
}
|
||||
|
||||
function intentRouterToForm(
|
||||
config: WechatIntentRouterAdminConfig | undefined,
|
||||
keys: LlmProviderKeyRow[],
|
||||
@@ -147,6 +184,13 @@ export function WechatPage() {
|
||||
const [savedIntentForm, setSavedIntentForm] = useState<IntentRouterForm | null>(null);
|
||||
const [intentRuntime, setIntentRuntime] = useState<WechatIntentRouterRuntimeState | null>(null);
|
||||
const [intentSaving, setIntentSaving] = useState(false);
|
||||
const [newsDraftForm, setNewsDraftForm] = useState<NewsMorningDraftForm | null>(null);
|
||||
const [savedNewsDraftForm, setSavedNewsDraftForm] = useState<NewsMorningDraftForm | null>(null);
|
||||
const [newsDraftTemplate, setNewsDraftTemplate] = useState<WechatNewsMorningDraftTemplate | null>(null);
|
||||
const [newsDraftPreview, setNewsDraftPreview] = useState<WechatNewsMorningDraftPreview | null>(null);
|
||||
const [newsDraftRuns, setNewsDraftRuns] = useState<WechatNewsMorningDraftRun[]>([]);
|
||||
const [newsDraftSaving, setNewsDraftSaving] = useState(false);
|
||||
const [newsDraftWorking, setNewsDraftWorking] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -158,6 +202,11 @@ export function WechatPage() {
|
||||
return JSON.stringify(intentForm) !== JSON.stringify(savedIntentForm);
|
||||
}, [intentForm, savedIntentForm]);
|
||||
|
||||
const newsDraftDirty = useMemo(() => {
|
||||
if (!newsDraftForm || !savedNewsDraftForm) return false;
|
||||
return JSON.stringify(newsDraftForm) !== JSON.stringify(savedNewsDraftForm);
|
||||
}, [newsDraftForm, savedNewsDraftForm]);
|
||||
|
||||
const selectedIntentKey = useMemo(
|
||||
() => llmKeys.find((item) => item.id === intentForm?.modelProviderKeyId) ?? null,
|
||||
[llmKeys, intentForm?.modelProviderKeyId],
|
||||
@@ -178,6 +227,8 @@ export function WechatPage() {
|
||||
nextLlmKeys,
|
||||
nextIntentConfig,
|
||||
nextIntentRuntime,
|
||||
nextNewsDraftConfig,
|
||||
nextNewsDraftRuns,
|
||||
] =
|
||||
await Promise.all([
|
||||
getWechatAdminSummary(),
|
||||
@@ -190,6 +241,8 @@ export function WechatPage() {
|
||||
listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]),
|
||||
getWechatIntentRouterConfig().catch(() => null),
|
||||
getWechatIntentRouterRuntime().catch(() => null),
|
||||
getWechatNewsMorningDraftConfig().catch(() => null),
|
||||
listWechatNewsMorningDraftRuns(10).catch(() => [] as WechatNewsMorningDraftRun[]),
|
||||
]);
|
||||
setSummary(nextSummary);
|
||||
setScheduleLlmEnabled(nextSummary.config.scheduleLlmEnabled ?? false);
|
||||
@@ -207,6 +260,13 @@ export function WechatPage() {
|
||||
setSavedIntentForm(nextForm);
|
||||
}
|
||||
setIntentRuntime(nextIntentRuntime);
|
||||
if (nextNewsDraftConfig) {
|
||||
const nextForm = newsMorningDraftToForm(nextNewsDraftConfig.config, nextNewsDraftConfig.template);
|
||||
setNewsDraftForm(nextForm);
|
||||
setSavedNewsDraftForm(nextForm);
|
||||
setNewsDraftTemplate(nextNewsDraftConfig.template);
|
||||
}
|
||||
setNewsDraftRuns(nextNewsDraftRuns);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载服务号管理失败');
|
||||
} finally {
|
||||
@@ -314,6 +374,82 @@ export function WechatPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewsMorningDraft = async () => {
|
||||
if (!newsDraftForm) return;
|
||||
setNewsDraftSaving(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await patchWechatNewsMorningDraftConfig({
|
||||
enabled: newsDraftForm.enabled,
|
||||
autoPushEnabled: newsDraftForm.autoPushEnabled,
|
||||
pushHour: Number(newsDraftForm.pushHour) || 6,
|
||||
pushMinute: Number(newsDraftForm.pushMinute) || 0,
|
||||
timezone: newsDraftForm.timezone.trim() || 'Asia/Shanghai',
|
||||
sourceUserId: newsDraftForm.sourceUserId.trim() || null,
|
||||
pageSlugPattern: newsDraftForm.pageSlugPattern.trim() || 'daily-news-*',
|
||||
author: newsDraftForm.author.trim() || 'TKMind',
|
||||
});
|
||||
const nextForm = newsMorningDraftToForm(result, newsDraftTemplate ?? undefined);
|
||||
setNewsDraftForm(nextForm);
|
||||
setSavedNewsDraftForm(nextForm);
|
||||
setNotice('新闻早报草稿推送配置已保存。');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setNewsDraftSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewNewsMorningDraft = async () => {
|
||||
setNewsDraftWorking(true);
|
||||
setError(null);
|
||||
try {
|
||||
setNewsDraftPreview(await previewWechatNewsMorningDraft());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '预览失败');
|
||||
} finally {
|
||||
setNewsDraftWorking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePushNewsMorningDraft = async () => {
|
||||
if (!newsDraftForm) return;
|
||||
if (!newsDraftForm.sourceUserId.trim()) {
|
||||
setError('请先填写来源用户 ID');
|
||||
return;
|
||||
}
|
||||
setNewsDraftWorking(true);
|
||||
setError(null);
|
||||
setNotice('正在推送到微信草稿箱,请稍候(约 5–15 秒)…');
|
||||
try {
|
||||
if (newsDraftDirty || !newsDraftForm.enabled) {
|
||||
const saved = await patchWechatNewsMorningDraftConfig({
|
||||
enabled: true,
|
||||
autoPushEnabled: newsDraftForm.autoPushEnabled,
|
||||
pushHour: Number(newsDraftForm.pushHour) || 6,
|
||||
pushMinute: Number(newsDraftForm.pushMinute) || 0,
|
||||
timezone: newsDraftForm.timezone.trim() || 'Asia/Shanghai',
|
||||
sourceUserId: newsDraftForm.sourceUserId.trim() || null,
|
||||
pageSlugPattern: newsDraftForm.pageSlugPattern.trim() || 'daily-news-*',
|
||||
author: newsDraftForm.author.trim() || 'TKMind',
|
||||
});
|
||||
const nextForm = newsMorningDraftToForm(saved, newsDraftTemplate ?? undefined);
|
||||
setNewsDraftForm(nextForm);
|
||||
setSavedNewsDraftForm(nextForm);
|
||||
}
|
||||
const result = await pushWechatNewsMorningDraft();
|
||||
setNotice(`草稿已写入微信后台,media_id:${result.draftMediaId ?? '—'}`);
|
||||
setNewsDraftPreview(result.preview ?? null);
|
||||
setNewsDraftRuns(await listWechatNewsMorningDraftRuns(10));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '推送失败');
|
||||
setNotice(null);
|
||||
} finally {
|
||||
setNewsDraftWorking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveIntentRouter = async () => {
|
||||
if (!intentForm) return;
|
||||
setIntentSaving(true);
|
||||
@@ -425,6 +561,225 @@ export function WechatPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{newsDraftForm ? (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>新闻早报 · 微信草稿箱</h2>
|
||||
<p className="muted" style={{ marginTop: 6 }}>
|
||||
将 MindSpace 生成的新闻早报 HTML 转为服务号图文草稿。模板版本 {newsDraftForm.templateVersion}(0910 版式)。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void handleSaveNewsMorningDraft()}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking || !newsDraftDirty}
|
||||
>
|
||||
{newsDraftSaving ? '保存中…' : newsDraftDirty ? '保存配置' : '已保存'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-form" style={{ marginTop: 16 }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newsDraftForm.enabled}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, enabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
启用新闻早报草稿推送
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newsDraftForm.autoPushEnabled}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking || !newsDraftForm.enabled}
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, autoPushEnabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
定时自动推送(需 Portal 侧调度读取本配置)
|
||||
</label>
|
||||
<label>
|
||||
推送时间(北京时间)
|
||||
<div className="wechat-toolbar" style={{ marginTop: 8 }}>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={23}
|
||||
value={newsDraftForm.pushHour}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, pushHour: event.target.value })
|
||||
}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span>:</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={59}
|
||||
value={newsDraftForm.pushMinute}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, pushMinute: event.target.value })
|
||||
}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
来源用户 ID
|
||||
<input
|
||||
value={newsDraftForm.sourceUserId}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
placeholder="a70ff537-8908-486e-9b6c-042e07cc25db"
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, sourceUserId: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
页面文件名匹配
|
||||
<input
|
||||
value={newsDraftForm.pageSlugPattern}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
placeholder="daily-news-*"
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, pageSlugPattern: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
草稿作者名(最多 8 字)
|
||||
<input
|
||||
value={newsDraftForm.author}
|
||||
maxLength={8}
|
||||
disabled={busy || newsDraftSaving || newsDraftWorking}
|
||||
onChange={(event) =>
|
||||
setNewsDraftForm((current) => current && { ...current, author: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{newsDraftTemplate ? (
|
||||
<details style={{ marginTop: 12 }}>
|
||||
<summary className="muted">0910 模板说明(taskSpec 参考)</summary>
|
||||
<pre className="admin-code-block" style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>
|
||||
{newsDraftTemplate.spec}
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
<div className="wechat-toolbar" style={{ marginTop: 16 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void handlePreviewNewsMorningDraft()}
|
||||
disabled={busy || newsDraftWorking || !newsDraftForm.sourceUserId.trim()}
|
||||
>
|
||||
{newsDraftWorking ? '处理中…' : '预览今日草稿'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-btn"
|
||||
onClick={() => void handlePushNewsMorningDraft()}
|
||||
disabled={busy || newsDraftWorking || !newsDraftForm.sourceUserId.trim()}
|
||||
title={
|
||||
!newsDraftForm.sourceUserId.trim()
|
||||
? '请先填写来源用户 ID'
|
||||
: !newsDraftForm.enabled
|
||||
? '建议勾选「启用新闻早报草稿推送」后再推送'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{newsDraftWorking ? '推送中…' : '立即推送到草稿箱'}
|
||||
</button>
|
||||
</div>
|
||||
{!newsDraftForm.enabled ? (
|
||||
<p className="muted" style={{ marginTop: 8 }}>
|
||||
提示:未勾选「启用」也可尝试推送;若失败请先启用并保存配置。
|
||||
</p>
|
||||
) : null}
|
||||
{newsDraftDirty ? (
|
||||
<p className="muted" style={{ marginTop: 8 }}>
|
||||
配置有未保存改动,点击推送时会自动保存后再推送到微信。
|
||||
</p>
|
||||
) : null}
|
||||
{newsDraftPreview ? (
|
||||
<div className="admin-card" style={{ marginTop: 16, background: 'var(--panel-2, rgba(255,255,255,.03))' }}>
|
||||
<h3 style={{ marginBottom: 8 }}>预览</h3>
|
||||
<dl className="admin-dl">
|
||||
<div>
|
||||
<dt>页面</dt>
|
||||
<dd>
|
||||
<a href={newsDraftPreview.page.publicUrl} target="_blank" rel="noreferrer">
|
||||
{newsDraftPreview.page.slug}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>标题</dt>
|
||||
<dd>{newsDraftPreview.article.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>摘要</dt>
|
||||
<dd>{newsDraftPreview.article.digest}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>正文模式</dt>
|
||||
<dd>
|
||||
{'contentMode' in newsDraftPreview.article && newsDraftPreview.article.contentMode === 'inline_html'
|
||||
? `内联 HTML(公众号排版,非长图${newsDraftPreview.article.trimmed ? ',已自动裁剪' : ''})`
|
||||
: 'HTML 正文'}
|
||||
</dd>
|
||||
</div>
|
||||
{'contentLength' in newsDraftPreview.article && newsDraftPreview.article.contentLength ? (
|
||||
<div>
|
||||
<dt>正文字符数</dt>
|
||||
<dd>{newsDraftPreview.article.contentLength.toLocaleString()} / 20,000</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{'cardCount' in newsDraftPreview.article ? (
|
||||
<div>
|
||||
<dt>卡片数</dt>
|
||||
<dd>{newsDraftPreview.article.cardCount}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
) : null}
|
||||
{newsDraftRuns.length > 0 ? (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<h3>最近推送记录</h3>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>状态</th>
|
||||
<th>页面</th>
|
||||
<th>draft media_id</th>
|
||||
<th>错误</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{newsDraftRuns.map((run) => (
|
||||
<tr key={run.id}>
|
||||
<td>{dateLabel(run.createdAt)}</td>
|
||||
<td className={statusClass(run.status)}>{statusLabel(run.status)}</td>
|
||||
<td>{run.pageSlug ?? '—'}</td>
|
||||
<td>{run.draftMediaId ?? '—'}</td>
|
||||
<td className={run.errorMessage ? 'text-error' : 'muted'}>{run.errorMessage ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{intentForm ? (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
|
||||
@@ -55,6 +55,10 @@ import type {
|
||||
WechatDeliveryLog,
|
||||
WechatDigestSubscription,
|
||||
WechatScheduleLlmConfig,
|
||||
WechatNewsMorningDraftConfig,
|
||||
WechatNewsMorningDraftPreview,
|
||||
WechatNewsMorningDraftRun,
|
||||
WechatNewsMorningDraftTemplate,
|
||||
WechatIntentRouterAdminConfig,
|
||||
WechatIntentRouterConfigState,
|
||||
WechatIntentRouterRuntimeState,
|
||||
@@ -458,6 +462,50 @@ export async function updateWechatScheduleLlmConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWechatNewsMorningDraftConfig(): Promise<{
|
||||
config: WechatNewsMorningDraftConfig;
|
||||
template: WechatNewsMorningDraftTemplate;
|
||||
}> {
|
||||
return portalFetch('/admin-api/wechat/news-morning-draft/config');
|
||||
}
|
||||
|
||||
export async function patchWechatNewsMorningDraftConfig(
|
||||
patch: Partial<Omit<WechatNewsMorningDraftConfig, 'updatedAt' | 'updatedBy' | 'templateVersion'>>,
|
||||
): Promise<WechatNewsMorningDraftConfig> {
|
||||
const result = await portalFetch<{ config: WechatNewsMorningDraftConfig }>(
|
||||
'/admin-api/wechat/news-morning-draft/config',
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
},
|
||||
);
|
||||
return result.config;
|
||||
}
|
||||
|
||||
export async function previewWechatNewsMorningDraft(): Promise<WechatNewsMorningDraftPreview> {
|
||||
return portalFetch<WechatNewsMorningDraftPreview>('/admin-api/wechat/news-morning-draft/preview');
|
||||
}
|
||||
|
||||
export async function pushWechatNewsMorningDraft(options?: { dryRun?: boolean }): Promise<{
|
||||
ok?: boolean;
|
||||
dryRun?: boolean;
|
||||
draftMediaId?: string;
|
||||
preview?: WechatNewsMorningDraftPreview;
|
||||
run?: WechatNewsMorningDraftRun;
|
||||
}> {
|
||||
return portalFetch('/admin-api/wechat/news-morning-draft/push', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listWechatNewsMorningDraftRuns(limit = 20): Promise<WechatNewsMorningDraftRun[]> {
|
||||
const result = await portalFetch<{ runs: WechatNewsMorningDraftRun[] }>(
|
||||
`/admin-api/wechat/news-morning-draft/runs?limit=${encodeURIComponent(String(limit))}`,
|
||||
);
|
||||
return result.runs ?? [];
|
||||
}
|
||||
|
||||
export async function getWechatIntentRouterConfig(): Promise<WechatIntentRouterAdminConfig> {
|
||||
const result = await portalFetch<WechatIntentRouterConfigState>('/admin-api/wechat/intent-router/config');
|
||||
return result.config;
|
||||
|
||||
@@ -330,6 +330,60 @@ export type WechatScheduleLlmConfig = {
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
export type WechatNewsMorningDraftConfig = {
|
||||
enabled: boolean;
|
||||
autoPushEnabled: boolean;
|
||||
pushHour: number;
|
||||
pushMinute: number;
|
||||
timezone: string;
|
||||
sourceUserId: string | null;
|
||||
pageSlugPattern: string;
|
||||
author: string;
|
||||
templateVersion: string;
|
||||
updatedAt: number | null;
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
export type WechatNewsMorningDraftTemplate = {
|
||||
version: string;
|
||||
spec: string;
|
||||
};
|
||||
|
||||
export type WechatNewsMorningDraftPreview = {
|
||||
config: WechatNewsMorningDraftConfig;
|
||||
page: {
|
||||
slug: string;
|
||||
relativePath: string;
|
||||
publicUrl: string;
|
||||
modifiedAt: number;
|
||||
hasThumb: boolean;
|
||||
};
|
||||
article: {
|
||||
title: string;
|
||||
digest: string;
|
||||
content: string;
|
||||
contentSourceUrl?: string;
|
||||
contentMode?: 'inline_html' | 'html' | string;
|
||||
contentLength?: number;
|
||||
cardCount?: number;
|
||||
statsCount?: number;
|
||||
trimmed?: boolean;
|
||||
trimLevel?: number;
|
||||
};
|
||||
template: WechatNewsMorningDraftTemplate;
|
||||
};
|
||||
|
||||
export type WechatNewsMorningDraftRun = {
|
||||
id: string;
|
||||
status: string;
|
||||
pageSlug: string | null;
|
||||
pageUrl: string | null;
|
||||
draftMediaId: string | null;
|
||||
errorMessage: string | null;
|
||||
triggeredBy: string | null;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type WechatIntentRouterAdminConfig = {
|
||||
enabled: boolean;
|
||||
shadowMode: boolean;
|
||||
|
||||
Reference in New Issue
Block a user