229805a070
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
203 lines
6.8 KiB
TypeScript
203 lines
6.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
getWechatAgentRouteStatus,
|
||
getWechatBindingStatus,
|
||
resetWechatAgentRoute,
|
||
type WechatAgentRouteStatus,
|
||
} from '../api/client';
|
||
import { buildWechatAuthorizeUrl } from '../utils/wechat';
|
||
|
||
function formatRouteTime(timestamp: number | null) {
|
||
if (!timestamp) return '暂无';
|
||
const date = new Date(timestamp);
|
||
if (Number.isNaN(date.getTime())) return '暂无';
|
||
const pad = (value: number) => String(value).padStart(2, '0');
|
||
return `${date.getMonth() + 1}-${date.getDate()} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||
}
|
||
|
||
export function WechatAccountButton({
|
||
returnTo,
|
||
onBeforeAction,
|
||
}: {
|
||
returnTo?: string;
|
||
onBeforeAction?: () => void;
|
||
}) {
|
||
const [bound, setBound] = useState<boolean | null>(null);
|
||
const [nickname, setNickname] = useState<string | null>(null);
|
||
const [debugOpen, setDebugOpen] = useState(false);
|
||
const [route, setRoute] = useState<WechatAgentRouteStatus | null>(null);
|
||
const [routeLoading, setRouteLoading] = useState(false);
|
||
const [routeError, setRouteError] = useState<string | null>(null);
|
||
const [resetting, setResetting] = useState(false);
|
||
|
||
useEffect(() => {
|
||
void getWechatBindingStatus().then((status) => {
|
||
if (!status.enabled) {
|
||
setBound(null);
|
||
return;
|
||
}
|
||
setBound(status.bound);
|
||
setNickname(status.nickname ?? null);
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!debugOpen) return;
|
||
let cancelled = false;
|
||
setRouteLoading(true);
|
||
setRouteError(null);
|
||
void getWechatAgentRouteStatus()
|
||
.then((status) => {
|
||
if (cancelled) return;
|
||
setRoute(status);
|
||
})
|
||
.catch((err) => {
|
||
if (cancelled) return;
|
||
setRouteError(err instanceof Error ? err.message : '获取公众号 Agent 状态失败');
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) setRouteLoading(false);
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [debugOpen]);
|
||
|
||
if (bound === null) return null;
|
||
|
||
if (bound) {
|
||
return (
|
||
<>
|
||
<button
|
||
type="button"
|
||
className="ghost-btn wechat-account-bound wechat-account-debug-trigger"
|
||
title="查看微信绑定与公众号 Agent 调试信息"
|
||
onClick={() => {
|
||
onBeforeAction?.();
|
||
setDebugOpen(true);
|
||
}}
|
||
>
|
||
微信已绑{nickname ? ` · ${nickname}` : ''}
|
||
</button>
|
||
|
||
{debugOpen && (
|
||
<div className="wechat-debug-overlay" onClick={() => setDebugOpen(false)}>
|
||
<section
|
||
className="wechat-debug-card"
|
||
onClick={(event) => event.stopPropagation()}
|
||
aria-modal="true"
|
||
role="dialog"
|
||
aria-label="公众号 Agent 调试"
|
||
>
|
||
<div className="wechat-debug-head">
|
||
<div>
|
||
<p className="wechat-debug-eyebrow">Wechat Agent</p>
|
||
<h3>公众号调试</h3>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="wechat-debug-close"
|
||
aria-label="关闭"
|
||
onClick={() => setDebugOpen(false)}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="wechat-debug-grid">
|
||
<div className="wechat-debug-item">
|
||
<span>微信绑定</span>
|
||
<strong>{nickname ? `已绑定 · ${nickname}` : '已绑定'}</strong>
|
||
</div>
|
||
<div className="wechat-debug-item">
|
||
<span>公众号通道</span>
|
||
<strong>
|
||
{route?.enabled ? '已启用' : routeLoading ? '读取中…' : '未启用'}
|
||
</strong>
|
||
</div>
|
||
<div className="wechat-debug-item">
|
||
<span>OpenID</span>
|
||
<code>{route?.openid ?? '暂无'}</code>
|
||
</div>
|
||
<div className="wechat-debug-item">
|
||
<span>Agent Session</span>
|
||
<code>{route?.agentSessionId ?? '暂无'}</code>
|
||
</div>
|
||
<div className="wechat-debug-item">
|
||
<span>路由状态</span>
|
||
<strong>{route?.routeStatus ?? '暂无'}</strong>
|
||
</div>
|
||
<div className="wechat-debug-item">
|
||
<span>最近刷新</span>
|
||
<strong>{formatRouteTime(route?.updatedAt ?? null)}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
{routeLoading && <p className="wechat-debug-hint">正在读取公众号 Agent 路由状态…</p>}
|
||
{routeError && <p className="wechat-debug-error">{routeError}</p>}
|
||
{!routeLoading && !routeError && route && !route.enabled && (
|
||
<p className="wechat-debug-hint">
|
||
当前环境未启用公众号消息转发,所以不会影响现有 H5 聊天。
|
||
</p>
|
||
)}
|
||
|
||
<div className="wechat-debug-actions">
|
||
<button
|
||
type="button"
|
||
className="ghost-btn"
|
||
disabled={routeLoading || resetting || !route?.enabled}
|
||
onClick={() => {
|
||
setRouteLoading(true);
|
||
setRouteError(null);
|
||
void getWechatAgentRouteStatus()
|
||
.then((status) => setRoute(status))
|
||
.catch((err) =>
|
||
setRouteError(err instanceof Error ? err.message : '刷新路由状态失败'),
|
||
)
|
||
.finally(() => setRouteLoading(false));
|
||
}}
|
||
>
|
||
刷新状态
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="ghost-btn"
|
||
disabled={routeLoading || resetting || !route?.enabled}
|
||
onClick={() => {
|
||
setResetting(true);
|
||
setRouteError(null);
|
||
void resetWechatAgentRoute()
|
||
.then((status) => setRoute(status))
|
||
.catch((err) =>
|
||
setRouteError(err instanceof Error ? err.message : '重建公众号 Agent 路由失败'),
|
||
)
|
||
.finally(() => setResetting(false));
|
||
}}
|
||
>
|
||
{resetting ? '重建中…' : '一键重建'}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<button
|
||
type="button"
|
||
className="ghost-btn"
|
||
onClick={() => {
|
||
onBeforeAction?.();
|
||
window.location.href = buildWechatAuthorizeUrl({
|
||
intent: 'bind',
|
||
returnTo: returnTo ?? window.location.pathname,
|
||
});
|
||
}}
|
||
>
|
||
绑定微信
|
||
</button>
|
||
);
|
||
}
|