mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 21:08:34 +03:00
fix(weixin): harden protocol delivery, streaming, and login (#5263)
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user