feat(mindspace): add WeChat MP config and one-click draft push on publish.

Let users bind their own official account credentials in M 配置 and push MindSpace pages to the WeChat draft box during or after publish, reusing the existing draft conversion pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 16:43:25 +08:00
parent 3bc2442769
commit 78b7d546c2
19 changed files with 1635 additions and 16 deletions
+128 -5
View File
@@ -18,6 +18,10 @@ import {
updateMindSpacePage,
updatePublicationStatus,
} from '../api/client';
import {
getMindSpaceWechatMpConfig,
pushMindSpacePageWechatDraft,
} from '../api/mindspace-wechat-mp';
import type {
MindSpacePage,
MindSpacePageDeletePreview,
@@ -319,16 +323,52 @@ export function MindSpacePageDetail({
const [plazaSubmitting, setPlazaSubmitting] = useState(false);
const [plazaPost, setPlazaPost] = useState<PlazaPostBrief | null>(null);
const [pushToPlaza, setPushToPlaza] = useState(false);
const [pushToWechatDraft, setPushToWechatDraft] = useState(false);
const [wechatMpConfigured, setWechatMpConfigured] = useState(false);
const [wechatDraftPushing, setWechatDraftPushing] = useState(false);
const [wechatDraftNotice, setWechatDraftNotice] = useState<string | null>(null);
const [fixNotice, setFixNotice] = useState<MindSpaceRedactionChange[] | null>(null);
const [publishSuccess, setPublishSuccess] = useState<{
publication: MindSpacePublication;
plazaPost: PlazaPostBrief | null;
pageTitle: string;
pushedToPlaza: boolean;
pushedToWechatDraft: boolean;
wechatDraftMediaId?: string | null;
wechatDraftError?: string | null;
} | null>(null);
const [confirmPublicationStatusOpen, setConfirmPublicationStatusOpen] = useState(false);
const [statusConfirming, setStatusConfirming] = useState(false);
useEffect(() => {
let cancelled = false;
void getMindSpaceWechatMpConfig()
.then((config) => {
if (!cancelled) setWechatMpConfigured(Boolean(config.configured));
})
.catch(() => {
if (!cancelled) setWechatMpConfigured(false);
});
return () => {
cancelled = true;
};
}, [pageId]);
useEffect(() => {
if (!publishOpen) return;
let cancelled = false;
void getMindSpaceWechatMpConfig()
.then((config) => {
if (!cancelled) setWechatMpConfigured(Boolean(config.configured));
})
.catch(() => {
if (!cancelled) setWechatMpConfigured(false);
});
return () => {
cancelled = true;
};
}, [publishOpen]);
useEffect(() => {
if (!publishOpen) return;
let cancelled = false;
@@ -559,6 +599,16 @@ export function MindSpacePageDetail({
if (pushToPlaza) {
nextPlazaPost = await publishToPlazaWithDefaults(publication.id);
}
let wechatDraftMediaId: string | null = null;
let wechatDraftError: string | null = null;
if (pushToWechatDraft) {
try {
const draftResult = await pushMindSpacePageWechatDraft(page.id);
wechatDraftMediaId = draftResult.draftMediaId;
} catch (err) {
wechatDraftError = err instanceof Error ? err.message : '推送到公众号草稿箱失败';
}
}
setPublishOpen(false);
setFullscreenPreviewOpen(false);
onFullscreenPreviewChange?.(false);
@@ -567,10 +617,19 @@ export function MindSpacePageDetail({
plazaPost: nextPlazaPost,
pageTitle: title || page.title,
pushedToPlaza: pushToPlaza,
pushedToWechatDraft: pushToWechatDraft,
wechatDraftMediaId,
wechatDraftError,
});
await onSaved();
} catch (err) {
setError(err instanceof Error ? err.message : pushToPlaza ? '发布或推送到 Plaza 失败' : '发布失败');
setError(
err instanceof Error
? err.message
: pushToPlaza || pushToWechatDraft
? '发布或后续推送失败'
: '发布失败',
);
} finally {
setPublishing(false);
}
@@ -645,6 +704,25 @@ export function MindSpacePageDetail({
}
};
const pushWechatDraft = async () => {
if (!page) return;
if (!wechatMpConfigured) {
setError('请先在 M 配置中绑定公众号 AppID 和 AppSecret。');
return;
}
setWechatDraftPushing(true);
setError(null);
setWechatDraftNotice(null);
try {
const result = await pushMindSpacePageWechatDraft(page.id);
setWechatDraftNotice(`已推送到公众号草稿箱。草稿 media_id:${result.draftMediaId}`);
} catch (err) {
setError(err instanceof Error ? err.message : '推送到公众号草稿箱失败');
} finally {
setWechatDraftPushing(false);
}
};
const submitPlaza = async () => {
if (!page?.publication || !plazaCategoryId) return;
setPlazaSubmitting(true);
@@ -986,6 +1064,9 @@ export function MindSpacePageDetail({
publication={publishSuccess.publication}
plazaPost={publishSuccess.plazaPost}
pushedToPlaza={publishSuccess.pushedToPlaza}
pushedToWechatDraft={publishSuccess.pushedToWechatDraft}
wechatDraftMediaId={publishSuccess.wechatDraftMediaId}
wechatDraftError={publishSuccess.wechatDraftError}
offlineBusy={publishing}
error={error}
onOffline={offlineFromSuccess}
@@ -1104,6 +1185,15 @@ export function MindSpacePageDetail({
)}
</MindSpaceModal>
{wechatDraftNotice ? (
<div className="mindspace-publication-live">
<div>
<span>公众号草稿箱</span>
<small>{wechatDraftNotice}</small>
</div>
</div>
) : null}
{page.publication && (
<div className="mindspace-publication-live">
<div>
@@ -1139,6 +1229,16 @@ export function MindSpacePageDetail({
发布到广场
</button>
) : null}
{page.publication.status === 'online' ? (
<button
type="button"
onClick={() => void pushWechatDraft()}
disabled={publishing || wechatDraftPushing || !wechatMpConfigured}
title={wechatMpConfigured ? undefined : '请先在 M 配置中绑定公众号凭证'}
>
{wechatDraftPushing ? '推送草稿箱中…' : '推送到公众号草稿箱'}
</button>
) : null}
<button
type="button"
onClick={() => {
@@ -1490,9 +1590,13 @@ export function MindSpacePageDetail({
)}
<div className="mindspace-page-editor-actions">
<span>
{pushToPlaza
? '确认发布后会同步推送到 Plaza 发现广场。'
: '发布会创建独立快照,后续修改草稿不会影响线上页面。'}
{pushToPlaza && pushToWechatDraft
? '确认发布后会同步推送到 Plaza,并写入你的公众号草稿箱。'
: pushToPlaza
? '确认发布后会同步推送到 Plaza 发现广场。'
: pushToWechatDraft
? '确认发布后会写入你的公众号草稿箱(需先在 M 配置中绑定凭证)。'
: '发布会创建独立快照,后续修改草稿不会影响线上页面。'}
</span>
<div className="mindspace-publish-action-group">
<button
@@ -1528,13 +1632,32 @@ export function MindSpacePageDetail({
/>
推送 Plaza
</label>
<label className="mindspace-publish-plaza-toggle">
<input
type="checkbox"
checked={pushToWechatDraft}
disabled={publishing || !wechatMpConfigured}
onChange={(event) => setPushToWechatDraft(event.target.checked)}
/>
推送到公众号草稿箱
</label>
{!wechatMpConfigured ? (
<p className="mindspace-page-data-publish-hint">
尚未配置公众号凭证。
<a href="/space/wechat-config">前往 M 配置</a>
</p>
) : null}
<button
type="button"
className="mindspace-primary"
onClick={() => void publish()}
disabled={publishing || !publishCheck?.allowed || publishPageDataBlocked}
>
{publishing ? (pushToPlaza ? '发布并推送…' : '发布中…') : '确认发布'}
{publishing
? pushToPlaza || pushToWechatDraft
? '发布并推送…'
: '发布中…'
: '确认发布'}
</button>
</div>
</div>
@@ -10,6 +10,9 @@ export function MindSpacePublishSuccess({
publication,
plazaPost,
pushedToPlaza = false,
pushedToWechatDraft = false,
wechatDraftMediaId = null,
wechatDraftError = null,
offlineBusy = false,
error,
onOffline,
@@ -19,6 +22,9 @@ export function MindSpacePublishSuccess({
publication: MindSpacePublication;
plazaPost?: PlazaPostBrief | null;
pushedToPlaza?: boolean;
pushedToWechatDraft?: boolean;
wechatDraftMediaId?: string | null;
wechatDraftError?: string | null;
offlineBusy?: boolean;
error?: string | null;
onOffline: () => void | Promise<void>;
@@ -50,6 +56,16 @@ export function MindSpacePublishSuccess({
尚未推送到 Plaza。公开页已生效,如需上广场请在页面详情中点击「发布到广场」。
</p>
)}
{pushedToWechatDraft && !wechatDraftError ? (
<p className="mindspace-publish-success-note">
已推送到公众号草稿箱{wechatDraftMediaId ? `(media_id:${wechatDraftMediaId})` : ''}。请登录微信公众平台审阅后发表。
</p>
) : null}
{wechatDraftError ? (
<div className="mindspace-security-note">
页面已发布,但公众号草稿箱推送失败:{wechatDraftError}
</div>
) : null}
{error ? <div className="mindspace-security-note">{error}</div> : null}
<div className="mindspace-publish-success-actions">
<a
+61 -9
View File
@@ -58,6 +58,7 @@ import {
import { PREVIEW_ASSETS, PREVIEW_PAGES, PREVIEW_SPACE } from '../dev/mindspacePreviewData';
import { MindSpaceFeedCard } from './MindSpaceFeedCard';
import { MindSpaceAchievementsPanel } from './MindSpaceAchievementsPanel';
import { MindSpaceWechatMpConfigPanel } from './MindSpaceWechatMpConfigPanel';
import { MindSpacePageDetail } from './MindSpacePageDetail';
import { MindSpaceModal } from './MindSpaceModal';
import { HealthMindSpacePanel } from './HealthMindSpacePanel';
@@ -508,6 +509,7 @@ function canGenerateWithAgent(asset: MindSpaceAsset) {
type MindSpaceRouteSync = {
pushHome: () => void;
pushAchievements: () => void;
pushWechatConfig: () => void;
pushCategory: (code: MindSpaceSaveCategory | MindSpaceCategory['code']) => void;
pushPage: (pageId: string) => void;
};
@@ -539,6 +541,7 @@ export function MindSpaceView({
initialPageId,
initialCategoryCode,
initialAchievementsOpen = false,
initialWechatConfigOpen = false,
onBack,
onLogout,
onOpenFeedback,
@@ -550,6 +553,7 @@ export function MindSpaceView({
initialPageId?: string | null;
initialCategoryCode?: MindSpaceSaveCategory | 'health' | null;
initialAchievementsOpen?: boolean;
initialWechatConfigOpen?: boolean;
onBack: () => void;
onLogout: () => void;
onOpenFeedback?: () => void;
@@ -603,6 +607,7 @@ export function MindSpaceView({
const [allPagesPageIndex, setAllPagesPageIndex] = useState(0);
const [allPagesLoading, setAllPagesLoading] = useState(false);
const [achievementsOpen, setAchievementsOpen] = useState(initialAchievementsOpen);
const [wechatConfigOpen, setWechatConfigOpen] = useState(initialWechatConfigOpen);
const [achievementsItems, setAchievementsItems] = useState<MindSpacePage[]>([]);
const [achievementsTotal, setAchievementsTotal] = useState(0);
const [achievementsPageIndex, setAchievementsPageIndex] = useState(0);
@@ -669,11 +674,11 @@ export function MindSpaceView({
}, [selectedPageId]);
useEffect(() => {
if (!allPagesOpen && !achievementsOpen && !selectedPageId) return;
if (!allPagesOpen && !achievementsOpen && !wechatConfigOpen && !selectedPageId) return;
window.requestAnimationFrame(() => {
window.scrollTo({ top: 0, left: 0 });
});
}, [allPagesOpen, achievementsOpen, selectedPageId]);
}, [allPagesOpen, achievementsOpen, wechatConfigOpen, selectedPageId]);
useEffect(() => {
if (!selectedPageId || !session?.id || previewMode || pageFullscreenPreviewOpen) return;
@@ -1002,6 +1007,7 @@ export function MindSpaceView({
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAchievementsOpen(false);
setWechatConfigOpen(false);
setAllPagesOpen(true);
setSelectedAllPageIds([]);
routeSync?.pushHome();
@@ -1041,6 +1047,7 @@ export function MindSpaceView({
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setWechatConfigOpen(false);
setAchievementsOpen(true);
setSelectedAchievementPageIds([]);
routeSync?.pushAchievements();
@@ -1053,6 +1060,22 @@ export function MindSpaceView({
routeSync?.pushHome();
};
const openWechatConfigPanel = () => {
setSelectedCategory(null);
setSelectedPageId(null);
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(false);
setWechatConfigOpen(true);
routeSync?.pushWechatConfig();
};
const closeWechatConfigPanel = () => {
setWechatConfigOpen(false);
routeSync?.pushHome();
};
const refreshAgentJobsSummary = async () => {
if (previewMode) return;
const { page } = await listMindSpaceAgentJobs({ limit: 1, offset: 0 });
@@ -1088,6 +1111,7 @@ export function MindSpaceView({
setNewPageOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(false);
setWechatConfigOpen(false);
setAgentJobsPanelOpen(true);
routeSync?.pushHome();
void loadAgentJobsPage(0);
@@ -1206,7 +1230,7 @@ export function MindSpaceView({
useEffect(() => {
if (previewMode) return;
const refreshSchedule = () => {
if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen) {
if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen && !wechatConfigOpen) {
void refreshSpaceQuietly();
}
};
@@ -1223,6 +1247,7 @@ export function MindSpaceView({
agentJobsPanelOpen,
allPagesOpen,
achievementsOpen,
wechatConfigOpen,
]);
useEffect(() => {
@@ -1230,11 +1255,11 @@ export function MindSpaceView({
}, [initialPageId]);
useEffect(() => {
if (!space || !initialCategoryCode || initialCategoryCode === 'draft' || initialAchievementsOpen) return;
if (!space || !initialCategoryCode || initialCategoryCode === 'draft' || initialAchievementsOpen || initialWechatConfigOpen) return;
if (initialCategoryCode === 'health' && !healthEnabled) return;
const category = space.categories.find((item) => item.code === initialCategoryCode);
if (category) void openCategory(category);
}, [space, initialCategoryCode, initialAchievementsOpen, healthEnabled]);
}, [space, initialCategoryCode, initialAchievementsOpen, initialWechatConfigOpen, healthEnabled]);
useEffect(() => {
if (!initialAchievementsOpen || previewMode) return;
@@ -1243,10 +1268,22 @@ export function MindSpaceView({
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setWechatConfigOpen(false);
setAchievementsOpen(true);
void loadAchievementsPage(0);
}, [initialAchievementsOpen, previewMode]);
useEffect(() => {
if (!initialWechatConfigOpen || previewMode) return;
setSelectedCategory(null);
setSelectedPageId(null);
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(false);
setWechatConfigOpen(true);
}, [initialWechatConfigOpen, previewMode]);
useEffect(() => {
if (previewMode || !agentJob || !['queued', 'running'].includes(agentJob.status)) return;
const timer = window.setInterval(() => {
@@ -2107,7 +2144,8 @@ export function MindSpaceView({
!newPageOpen &&
!agentJobsPanelOpen &&
!allPagesOpen &&
!achievementsOpen;
!achievementsOpen &&
!wechatConfigOpen;
const rawUpcomingReminders = previewMode ? [] : space?.schedule?.upcomingReminders ?? [];
const scheduleReminders = previewMode
? PREVIEW_SCHEDULE_REMINDERS.map((item) => ({ ...item }))
@@ -2247,7 +2285,7 @@ export function MindSpaceView({
</header>
<main className="mindspace-content">
{!allPagesOpen && !achievementsOpen && (
{!allPagesOpen && !achievementsOpen && !wechatConfigOpen && (
<section className="mindspace-hero">
<div className="mindspace-hero-left">
<p className="mindspace-eyebrow">MINDSPACE</p>
@@ -2270,6 +2308,7 @@ export function MindSpaceView({
setSelectedPageId(null);
setAllPagesOpen(false);
setAchievementsOpen(false);
setWechatConfigOpen(false);
setNewPageOpen(true);
}}
>
@@ -2544,7 +2583,7 @@ export function MindSpaceView({
<section className="mindspace-page-reader">
<header className="mindspace-page-reader-bar">
<button type="button" className="mindspace-inline-back" onClick={closePage}>
{allPagesOpen ? '返回全部页面' : achievementsOpen ? '返回成果展示' : '返回空间首页'}
{allPagesOpen ? '返回全部页面' : achievementsOpen ? '返回成果展示' : wechatConfigOpen ? '返回 M 配置' : '返回空间首页'}
</button>
<span>站内浏览</span>
</header>
@@ -2626,6 +2665,8 @@ export function MindSpaceView({
</button>
</div>
</section>
) : wechatConfigOpen ? (
<MindSpaceWechatMpConfigPanel onBack={closeWechatConfigPanel} />
) : achievementsOpen ? (
<MindSpaceAchievementsPanel
items={achievementsItems}
@@ -3233,6 +3274,17 @@ export function MindSpaceView({
))}
</div>
<div className="mindspace-grid-secondary">
<article className="mindspace-card mindspace-card-compact mindspace-card-wechat-config">
<div className="mindspace-card-top">
<span className="mindspace-card-code">WECHAT</span>
<span>草稿箱</span>
</div>
<h3>M 配置</h3>
<p>绑定你的服务号或订阅号 AppID / AppSecret,用于一键推送到草稿箱。</p>
<button type="button" onClick={openWechatConfigPanel}>
打开配置
</button>
</article>
<article className="mindspace-card mindspace-card-compact mindspace-card-achievements">
<div className="mindspace-card-top">
<span className="mindspace-card-code">SHOWCASE</span>
@@ -3269,7 +3321,7 @@ export function MindSpaceView({
</section>
)}
{!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen && (
{!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen && !wechatConfigOpen && (
<section className="mindspace-section mindspace-recent-work">
<div className="mindspace-section-heading">
<div>
@@ -0,0 +1,230 @@
import { useEffect, useState } from 'react';
import {
deleteMindSpaceWechatMpConfig,
getMindSpaceWechatMpConfig,
saveMindSpaceWechatMpConfig,
verifyMindSpaceWechatMpConfig,
type MindSpaceWechatMpAccountType,
type MindSpaceWechatMpConfig,
} from '../api/mindspace-wechat-mp';
function formatTime(value: number | null | undefined) {
if (!value) return '—';
return new Date(value).toLocaleString('zh-CN', { hour12: false });
}
type MindSpaceWechatMpConfigPanelProps = {
onBack: () => void;
};
export function MindSpaceWechatMpConfigPanel({ onBack }: MindSpaceWechatMpConfigPanelProps) {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [verifying, setVerifying] = useState(false);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [config, setConfig] = useState<MindSpaceWechatMpConfig | null>(null);
const [appId, setAppId] = useState('');
const [appSecret, setAppSecret] = useState('');
const [accountType, setAccountType] = useState<MindSpaceWechatMpAccountType>('service');
const [label, setLabel] = useState('');
const [author, setAuthor] = useState('');
const load = async () => {
setLoading(true);
setError(null);
try {
const next = await getMindSpaceWechatMpConfig();
setConfig(next);
setAppId(next.appId ?? '');
setAccountType(next.accountType ?? 'service');
setLabel(next.label ?? '');
setAuthor(next.author ?? '');
setAppSecret('');
} catch (err) {
setError(err instanceof Error ? err.message : '加载公众号配置失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const save = async () => {
setSaving(true);
setError(null);
setNotice(null);
try {
const next = await saveMindSpaceWechatMpConfig({
appId: appId.trim(),
appSecret: appSecret.trim() || undefined,
accountType,
label: label.trim(),
author: author.trim(),
});
setConfig(next);
setAppSecret('');
setNotice('配置已保存并通过连通性校验。');
} catch (err) {
setError(err instanceof Error ? err.message : '保存公众号配置失败');
} finally {
setSaving(false);
}
};
const verify = async () => {
setVerifying(true);
setError(null);
setNotice(null);
try {
const next = await verifyMindSpaceWechatMpConfig();
setConfig(next);
setNotice('凭证校验成功。');
} catch (err) {
setError(err instanceof Error ? err.message : '凭证校验失败');
} finally {
setVerifying(false);
}
};
const remove = async () => {
if (!window.confirm('确定删除公众号配置?删除后无法一键推送到草稿箱。')) return;
setDeleting(true);
setError(null);
setNotice(null);
try {
await deleteMindSpaceWechatMpConfig();
setConfig({
configured: false,
appId: '',
accountType: 'service',
label: '',
author: '',
appSecretMasked: '',
verifiedAt: null,
updatedAt: null,
});
setAppId('');
setAppSecret('');
setLabel('');
setAuthor('');
setNotice('已删除公众号配置。');
} catch (err) {
setError(err instanceof Error ? err.message : '删除公众号配置失败');
} finally {
setDeleting(false);
}
};
return (
<section className="mindspace-independent-page mindspace-wechat-config">
<div className="mindspace-section-heading">
<div>
<button type="button" className="mindspace-inline-back" onClick={onBack}>
返回空间首页
</button>
<p className="mindspace-eyebrow">WECHAT MP</p>
<h2>M 配置</h2>
</div>
</div>
<p className="mindspace-wechat-config-intro">
填写你的服务号或订阅号 AppID 与 AppSecret。保存后,可在页面发布时一键推送到公众号草稿箱。
请先在微信公众平台配置服务器 IP 白名单,否则无法获取 access_token。
</p>
{loading ? (
<div className="mindspace-state">正在加载配置…</div>
) : (
<section className="mindspace-publish-panel mindspace-wechat-config-form">
<label>
账号类型
<select
value={accountType}
onChange={(event) => setAccountType(event.target.value as MindSpaceWechatMpAccountType)}
>
<option value="service">服务号</option>
<option value="subscription">订阅号</option>
</select>
</label>
<label>
备注名称(可选)
<input
value={label}
maxLength={64}
onChange={(event) => setLabel(event.target.value)}
placeholder="例如:公司服务号"
/>
</label>
<label>
AppID
<input
value={appId}
maxLength={64}
onChange={(event) => setAppId(event.target.value)}
placeholder="wxXXXXXXXXXXXXXXXX"
autoComplete="off"
/>
</label>
<label>
AppSecret
<input
value={appSecret}
type="password"
maxLength={128}
onChange={(event) => setAppSecret(event.target.value)}
placeholder={config?.configured ? `已保存:${config.appSecretMasked}` : '请输入 AppSecret'}
autoComplete="new-password"
/>
</label>
<label>
草稿作者名(可选,最多 8 字)
<input
value={author}
maxLength={8}
onChange={(event) => setAuthor(event.target.value)}
placeholder="TKMind"
/>
</label>
{config?.configured ? (
<div className="mindspace-wechat-config-meta">
<span>最近校验:{formatTime(config.verifiedAt)}</span>
<span>最近更新:{formatTime(config.updatedAt)}</span>
</div>
) : null}
{error ? <div className="mindspace-security-note">{error}</div> : null}
{notice ? <div className="mindspace-publish-success-note">{notice}</div> : null}
<div className="mindspace-page-editor-actions">
<span>保存时会自动校验凭证;Secret 加密存储,不会明文回显。</span>
<div className="mindspace-publish-action-group">
{config?.configured ? (
<>
<button type="button" onClick={() => void verify()} disabled={verifying || saving || deleting}>
{verifying ? '校验中…' : '重新校验'}
</button>
<button type="button" onClick={() => void remove()} disabled={deleting || saving || verifying}>
{deleting ? '删除中…' : '删除配置'}
</button>
</>
) : null}
<button
type="button"
className="mindspace-primary"
onClick={() => void save()}
disabled={saving || verifying || deleting || !appId.trim() || (!config?.configured && !appSecret.trim())}
>
{saving ? '保存中…' : '保存配置'}
</button>
</div>
</div>
</section>
)}
</section>
);
}