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:
@@ -55,6 +55,7 @@ export function resolveProductRouteName(pathname: string) {
|
||||
if (pathname === '/') return 'chat';
|
||||
if (pathname === '/space') return 'mindspace_home';
|
||||
if (pathname === '/space/achievements') return 'mindspace_achievements';
|
||||
if (pathname === '/space/wechat-config') return 'mindspace_wechat_config';
|
||||
if (pathname.startsWith('/space/page/')) return 'mindspace_page';
|
||||
if (pathname.startsWith('/feedback/')) return 'feedback_detail';
|
||||
if (pathname === '/feedback') return 'feedback';
|
||||
|
||||
@@ -110,6 +110,14 @@ export {
|
||||
redactMindSpacePage,
|
||||
updatePublicationStatus,
|
||||
} from './mindspace-publications';
|
||||
export {
|
||||
deleteMindSpaceWechatMpConfig,
|
||||
getMindSpaceWechatMpConfig,
|
||||
listMindSpaceWechatPageDraftRuns,
|
||||
pushMindSpacePageWechatDraft,
|
||||
saveMindSpaceWechatMpConfig,
|
||||
verifyMindSpaceWechatMpConfig,
|
||||
} from './mindspace-wechat-mp';
|
||||
export {
|
||||
applyPageDataPublishPolicy,
|
||||
buildPageDataExportUrl,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { apiFetch } from './core';
|
||||
|
||||
export type MindSpaceWechatMpAccountType = 'service' | 'subscription';
|
||||
|
||||
export type MindSpaceWechatMpConfig = {
|
||||
configured: boolean;
|
||||
appId: string;
|
||||
accountType: MindSpaceWechatMpAccountType;
|
||||
label: string;
|
||||
author: string;
|
||||
appSecretMasked: string;
|
||||
verifiedAt: number | null;
|
||||
updatedAt: number | null;
|
||||
};
|
||||
|
||||
export type MindSpaceWechatPageDraftRun = {
|
||||
id: string;
|
||||
pageId: string;
|
||||
status: 'success' | 'failed' | string;
|
||||
pageTitle: string | null;
|
||||
pageUrl: string | null;
|
||||
draftMediaId: string | null;
|
||||
errorMessage: string | null;
|
||||
triggeredBy: string | null;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type MindSpaceWechatPageDraftResult = {
|
||||
ok: boolean;
|
||||
draftMediaId: string;
|
||||
pageTitle: string;
|
||||
pageUrl: string;
|
||||
run: MindSpaceWechatPageDraftRun;
|
||||
};
|
||||
|
||||
export async function getMindSpaceWechatMpConfig(): Promise<MindSpaceWechatMpConfig> {
|
||||
const result = await apiFetch<{ data: MindSpaceWechatMpConfig }>('/mindspace/v1/wechat-mp/config');
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function saveMindSpaceWechatMpConfig(input: {
|
||||
appId: string;
|
||||
appSecret?: string;
|
||||
accountType?: MindSpaceWechatMpAccountType;
|
||||
label?: string;
|
||||
author?: string;
|
||||
verify?: boolean;
|
||||
}): Promise<MindSpaceWechatMpConfig> {
|
||||
const result = await apiFetch<{ data: MindSpaceWechatMpConfig }>('/mindspace/v1/wechat-mp/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
app_id: input.appId,
|
||||
app_secret: input.appSecret,
|
||||
account_type: input.accountType,
|
||||
label: input.label,
|
||||
author: input.author,
|
||||
verify: input.verify,
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function verifyMindSpaceWechatMpConfig(): Promise<MindSpaceWechatMpConfig> {
|
||||
const result = await apiFetch<{ data: MindSpaceWechatMpConfig }>(
|
||||
'/mindspace/v1/wechat-mp/config/verify',
|
||||
{ method: 'POST' },
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function deleteMindSpaceWechatMpConfig(): Promise<{ ok: boolean }> {
|
||||
const result = await apiFetch<{ data: { ok: boolean } }>('/mindspace/v1/wechat-mp/config', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function pushMindSpacePageWechatDraft(pageId: string): Promise<MindSpaceWechatPageDraftResult> {
|
||||
const result = await apiFetch<{ data: MindSpaceWechatPageDraftResult }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/wechat-draft`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function listMindSpaceWechatPageDraftRuns(
|
||||
pageId: string,
|
||||
limit = 10,
|
||||
): Promise<MindSpaceWechatPageDraftRun[]> {
|
||||
const result = await apiFetch<{ data: MindSpaceWechatPageDraftRun[] }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/wechat-draft/runs?limit=${limit}`,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -8296,6 +8296,25 @@ body,
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mindspace-wechat-config-intro {
|
||||
margin: 0 0 18px;
|
||||
color: rgba(24, 33, 29, 0.72);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mindspace-wechat-config-form {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.mindspace-wechat-config-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 18px;
|
||||
color: rgba(24, 33, 29, 0.62);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mindspace-achievements-bulkbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function MindSpaceRoute({
|
||||
const [searchParams] = useSearchParams();
|
||||
const pageMatch = matchPath('/space/page/:pageId', location.pathname);
|
||||
const achievementsMatch = matchPath('/space/achievements', location.pathname);
|
||||
const wechatConfigMatch = matchPath('/space/wechat-config', location.pathname);
|
||||
const pageId = pageMatch?.params.pageId ?? null;
|
||||
const categoryCode = parseCategory(searchParams.get('category'), healthEnabled);
|
||||
|
||||
@@ -37,12 +38,14 @@ export function MindSpaceRoute({
|
||||
initialPageId={pageId}
|
||||
initialCategoryCode={categoryCode}
|
||||
initialAchievementsOpen={Boolean(achievementsMatch)}
|
||||
initialWechatConfigOpen={Boolean(wechatConfigMatch)}
|
||||
onBack={() => navigate('/')}
|
||||
onLogout={onLogout}
|
||||
onOpenFeedback={() => navigate('/feedback')}
|
||||
routeSync={{
|
||||
pushHome: () => navigate('/space'),
|
||||
pushAchievements: () => navigate('/space/achievements'),
|
||||
pushWechatConfig: () => navigate('/space/wechat-config'),
|
||||
pushCategory: (code) => navigate(`/space?category=${code}`),
|
||||
pushPage: (id) => navigate(`/space/page/${id}`),
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user