fix(weixin): harden protocol delivery, streaming, and login (#5263)

This commit is contained in:
chengyongru
2026-08-07 16:53:31 +08:00
committed by GitHub
parent edb3b7e446
commit 332c159b93
23 changed files with 2858 additions and 238 deletions
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import QRCode from "qrcode";
import { Check, Loader2, Network, RotateCcw } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -34,6 +34,14 @@ export type ChannelConnectStartOptions = {
force?: boolean;
};
export type ChannelQrConnectPendingContext = {
connect: ChannelConnectPayload;
busy: boolean;
poll: (
params?: Readonly<Record<string, string>>,
) => Promise<ChannelConnectPayload | null>;
};
export function ChannelQrConnectFlow({
token,
channelName,
@@ -43,6 +51,10 @@ export function ChannelQrConnectFlow({
forceOnRepeat = false,
labels,
onFeaturesUpdate,
pausePolling,
renderPending,
resolveMessage,
suppressSucceeded = false,
}: {
token: string;
channelName: string;
@@ -52,6 +64,10 @@ export function ChannelQrConnectFlow({
forceOnRepeat?: boolean;
labels: ChannelQrConnectLabels;
onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void;
pausePolling?: (payload: ChannelConnectPayload) => boolean;
renderPending?: (context: ChannelQrConnectPendingContext) => ReactNode;
resolveMessage?: (payload: ChannelConnectPayload) => string | undefined;
suppressSucceeded?: boolean;
}) {
const pageVisible = usePageVisibility();
const { t } = useTranslation();
@@ -72,6 +88,10 @@ export function ChannelQrConnectFlow({
const pending = connect?.status === "pending";
const succeeded = connect?.status === "succeeded";
const canStart = !pending && !busy;
const pollingPaused = Boolean(connect && pausePolling?.(connect));
const displayMessage = connect
? resolveMessage?.(connect) ?? connect.message
: undefined;
useEffect(() => {
if (!connect?.qr_url) {
@@ -96,8 +116,14 @@ export function ChannelQrConnectFlow({
}, [connect?.qr_url]);
useEffect(() => {
if (!connect?.session_id || connect.status !== "pending" || !pageVisible) return;
if (
!connect?.session_id
|| connect.status !== "pending"
|| pollingPaused
|| !pageVisible
) return;
let cancelled = false;
const sessionId = connect.session_id;
const poll = async () => {
if (pollInFlight.current) return;
pollInFlight.current = true;
@@ -105,7 +131,7 @@ export function ChannelQrConnectFlow({
const payload = await pollChannelConnect(
tokenRef.current,
channelName,
connect.session_id,
sessionId,
);
if (cancelled) return;
setConnect((current) => ({
@@ -142,6 +168,7 @@ export function ChannelQrConnectFlow({
connect?.status,
onFeaturesUpdate,
pageVisible,
pollingPaused,
]);
const start = useCallback(async (force = false) => {
@@ -188,6 +215,40 @@ export function ChannelQrConnectFlow({
}
};
const submitPoll = async (
params: Readonly<Record<string, string>> = {},
): Promise<ChannelConnectPayload | null> => {
if (!connect?.session_id) return null;
setBusy(true);
setError(null);
try {
const payload = await pollChannelConnect(
tokenRef.current,
channelName,
connect.session_id,
"",
params,
);
setConnect((current) => ({
...(current ?? payload),
...payload,
qr_url: payload.qr_url ?? current?.qr_url,
}));
if (payload.nanobot_features) {
onFeaturesUpdate(payload.nanobot_features);
}
if (payload.status !== "pending") {
setError(null);
}
return payload;
} catch (err) {
setError((err as Error).message);
return null;
} finally {
setBusy(false);
}
};
return (
<div className="mt-3 space-y-3">
{pending ? (
@@ -210,10 +271,12 @@ export function ChannelQrConnectFlow({
<p className="mt-1 text-[12.5px] leading-5 text-muted-foreground">
{labels.scanDescription}
</p>
<div className="mt-3 flex items-center gap-2 text-[12px] text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
{labels.waiting}
</div>
{renderPending?.({ connect, busy, poll: submitPoll }) ?? (
<div className="mt-3 flex items-center gap-2 text-[12px] text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
{labels.waiting}
</div>
)}
<div className="mt-4 flex flex-wrap justify-end gap-2">
<Button
type="button"
@@ -230,16 +293,16 @@ export function ChannelQrConnectFlow({
</div>
) : null}
{succeeded ? (
{succeeded && !suppressSucceeded ? (
<div className="flex items-center gap-2 rounded-[12px] border border-emerald-500/20 px-3 py-2 text-[12px] font-medium text-emerald-700 dark:text-emerald-200">
<Check className="h-3.5 w-3.5" aria-hidden />
{connect.message ?? labels.connected}
{displayMessage ?? labels.connected}
</div>
) : null}
{connect && ["expired", "failed", "cancelled"].includes(connect.status) ? (
<div className="rounded-[12px] border border-border/60 px-3 py-2 text-[12px] leading-5 text-muted-foreground">
{connect.message || labels.stopped}
{displayMessage || labels.stopped}
</div>
) : null}
+4
View File
@@ -627,9 +627,13 @@ export async function pollChannelConnect(
channel: string,
sessionId: string,
base: string = "",
params: Readonly<Record<string, string>> = {},
): Promise<ChannelConnectPayload> {
const query = new URLSearchParams();
query.set("session_id", sessionId);
Object.entries(params).forEach(([key, value]) => {
if (key !== "session_id") query.set(key, value);
});
return request<ChannelConnectPayload>(
`${base}/api/settings/channels/${channel}/connect/poll?${query}`,
token,